Session,CSRF,中间件

发布时间 2023-08-04 00:44:30作者: 猪油哥

本节知识点概要
- Session
- CSRF
- Model操作
- Form验证(ModelForm)
- 中间件
- 缓存
- 信号

一、 Django内容回顾

1、 基础生命周期(补充):从请求到URL,到函数或类,返回字符串给用户

2、 URL中主要的4种

/index/     index
/list/(\d+)     index()
/list/(\d+)  name="li"   index(),在 views 中根据 name 反生成 URL
/list/(\d+)  include    index

3、 views中请求的数据:请求体和请求头

request.body(所有内容的原生值都在这里)
    request.POST(request.POST的源码可知request.POST数据是从request.body中提取出来的)
    request.FILES(request.FILES的源码可知request.FILES数据是从request.body中提取出来的)
    request.GET
    request.XXX.getlist

request.Meta(请求头数据,比如客户端的系统平台)
    request.path_info
    request.COOKIES
    request.method(POST方式自动将数据放在request.POSTGET方式->request.GET,如果PUTDELETE方式要从request.body中拿数据)

返回数据常用的3种方式:

a = "成都"
return HttpResponse,返回内容可以是字符串,也可以是字节,如 return HttpResponse(byte(a))
return render
return redirect

返回的时候还可以返回 cookie 给用户,是放在响应头(Response Header)中返回给用户比如:

response = HttpResponse(a)
response['name'] = 'michael'    # 这样设置后在页面显示的是 成都,但是在Response Header中有name=michael键值对
response.set_cookie()
return response

请求一个网站,比如 baidu.com/?,这种 get 方式请求的username和pwd直接放在URL中发送给服务器。如果是 post 请求,会有请求头信息,接着两个换行符后就是请求的内容信息,这个请求的内容信息以字符串形式发送到后台,比如"username=root;pwd=123",Django的 request.body 将这个字符串转化为字典,此时用 request.POST才能更方便获取。

4、 Model操作(原生的SQL语句也可以)

表内容操作:(假设有 TB 表)
models.TB.objects.create()
models.TB.objects.create(**{})
obj = models.TB(..)
obj.save()

models.TB.objects.all()[5:10]   # 切片

models.TB.objects.all
models.TB.objects.update(..)

models.TB.objects.filter(id__in=[1,2,3])

models.TB.objects.filter()
models.TB.objects.filter(单下划线id)
models.TB.objects.filter(...).delete
models.TB.objects.delete
models.TB.objects.values
models.TB.objects.values_list
models.TB.objects.get
models.TB.objects.filter().update()
models.TB.objects.filter().first()
models.TB.objects.filter(**{})
models.TB.objects.filter(**{}).count()
models.TB.objects.filter(双下划线跨表)
models.TB.objects.filter(id__gt=1)
models.TB.objects.filter(id__range=[1,5])   # 表示一个范围
models.TB.objects.filter(id__lt=1)
models.TB.objects.filter(id__lte=1)
models.TB.objects.filter(id__gte=1)
models.TB.objects.exclude(id__gte=1)

多对多:
obj.set
obj.add(1,2,3)
obj.add([1,2,3])
obj.remove([1,2,3])
obj.clear()
obj.all()

models.TB.objects.all()
[obj, obj, ...]
obj.fk.name

models.TB.objects.all().order_by("")
models.TB.objects.distinct()

一对多操作,现在假设有A表和B表,代码如下:
class A:
    name ..
    # b_set

class B:
    caption ..
    fk = ForignKey(A)   # 外键关联 A表
B表通过外键 fk 可以查询到 A表。那么A表怎样才能查询到B表呢,在A表通过 b_set 操作B表,这个 b_set 中的 b 就是B表的小写
字母。有 ForignKey可以跨表查询,没有ForignKey也可以通过 b_set 这样的方式跨表。

多对多操作,会创建第三张表,通过第三张表进行操作。

5、 模板语言
(1)、 基本操作

比如render方法传递了字典、列表给前端,前端可以通过点(.)来获取,例如:
return render(request, "index.html", {'k1':[1,2,3]})
在 index.html中获取方式:
<h1>{{ k1.0 }}</h1>

(2)、 继承模板:
extends "layout.html" # 后面是模板路径

(3)、 include 组件

(4)、 自定义方法 simple_tag, filter
simple_tag 不能用于判断,参数可以用空格分隔开。 filter最多只能传两个参数,参数间不能空格分隔。

二、 Session

cookie做用户认证时不安全,容易被别人看见。基于Cookie做用户认证时:敏感信息不适合放在cookie中。

1、 Session原理
Cookie是保存在用户浏览器端的键值对
Session是保存在服务器端的键值对

假设有用户登录后,在服务器端记录登录信息,但当用户退出登录后,再一次登录服务器怎样才知道是不是同一个用户呢。在服务器端的会话会保存用户的所有信息,服务端发送一个随机字符串给用户并保存这个随机字符串,例如:

session = {
    随机字符串1{
        'is_login': True,
        'user': '''
        'nid':
        ....
    },
    随机字符串2:{            # 另一个用户的信息
        'is_login': True,
        'user': '''
        'nid':
        ....
    },
}

用户端的浏览器只有这个随机字符串,没有敏感信息,用户来的时候带上随机字符串,服务端通过随机字符串获取用户信息。为进一步对这个 Session的理解,现在创建一个Django的项目文件 michael_04,并且在项目文件夹下创建app01,创建过程省略。

首先在settings.py文件中添加静态文件路径,并且在项目文件夹下创建静态文件夹(static):
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

修改 michael_04\urls.py文件,代码如下:

from django.contrib import admin
from django.urls import path
from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
    path('index/', views.index),
]

接下来在 app01\views.py 文件中的 login()函数和 index() 函数代码如下所示:

from django.shortcuts import render, redirect, HttpResponse
def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == "POST":
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        if user == 'root' and pwd == '123':
            request.session['username'] = user  # 设置session只要这一句就行
            return redirect("/index/")
def index(request):
    return HttpResponse("OK")

此时运行 michael_04,在浏览器地址栏输入 127.0.0.1:8000/index/,页面同样会显示 OK。还没有做到需要登录才查看index的页面。现在用会话来做,还需要做下面几个步骤:
a.生成随机字符串
b.写到用户浏览器 cookie
c.保存在服务端的 session 中
d.在随机字符对应的字典中设置相关内容

在用 session之前要先执行下面两条命令:
python manage.py makemigrations
python manage.py migrate


现在在浏览器地址栏输入 127.0.0.1:8000/login/ 在 Network 选项卡下看请求信息,在 cookie 中没有 cookie 的相关信息,当在页面上输入 root 和 123提交后,此时就会看到有 cookie 生成,名称是 sessionid,值是一个随机字符串,这个随机字符串是Django生成的。这个随机字符串默认情况下Django是保存在数据库中。连接michael_04下面的数据库文件可以看到这个session值。如下图1-1所示。

图1-1 数据库中的session随机字符串

从图1-1中可以看出,session_data是加密的数据,这个数据其实就是 username=root。在index() 函数中可以获取相应的 session值进行判断用户是否已经登录,如果已经登录就返回正确的信息,如果是未登录就返回不正确的数据。修改后的 login()函数和index()函数代码如下所示:

def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == "POST":
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        if user == 'root' and pwd == '123':
            request.session['username'] = user  # 在session设置值
            request.session['is_login'] = True  # 设置一个已经登录的标志
            return redirect("/index/")
        else:
            return render(request, 'login.html')

def index(request):
    # 获取当前用户的随机字符串
    # 根据随机字符串获取对应信息
    if request.session['is_login']:     # 判断是否已经登录
        return HttpResponse(request.session['username'])    # 在session获取值
    else:
        return HttpResponse('未登录')

templates\login.html 文件的代码如下所示:

<body>
    <form action="/login/" method="POST">
        <input type="text" name="user" />
        <input type="text" name="pwd" />
        <input type="submit" value="提交" />
    </form>
</body>

要让设置的 session 生效,还需要再一次执行下面的两条命令:
python manage.py makemigrations
python manage.py migrate

此时用 session 也可以完成验证功能。

2、 关于session的操作
关于 session 的操作,大致有下面这些:

# 获取、设置、删除Session中数据
request.session['k1']   # 获取k1 不存在要报错
request.session.get('k1',None)  # 获取k1,没有返回 None

request.session['k1'] = 123     # 设置值,有就更新
request.session.setdefault('k1',123) # 存在则不设置

del request.session['k1']       # 删除值
request.session.clear()
request.session.delete("session_key")   # 删除当前用户的所有Session数据

# 所有 键、值、键值对
request.session.keys()
request.session.values()
request.session.items()
request.session.iterkeys()
request.session.itervalues()
request.session.iteritems()

# 用户session的随机字符串
request.session.session_key     # 当前用户的随机字符串

# 将所有Session失效日期小于当前日期的数据删除
request.session.clear_expired()

# 检查用户session的随机字符串 在数据库中是否存在
request.session.exists("session_key")

request.session.set_expiry(value)
    * 如果value是个整数,session会在这些秒数后失效。
    * 如果value是个datatime或timedelta,session就会在这个时间后失效。
    * 如果value是0,用户关闭浏览器session就会失效。
    * 如果value是None,session会依赖全局session失效策略。

Django默认支持Session,并且默认是将Session数据存储在数据库中,即:django_session 表中。

a. 配置 settings.py
    SESSION_ENGINE = 'django.contrib.sessions.backends.db'   # 引擎(默认),将 session 放在数据库
    SESSION_COOKIE_NAME = "sessionid"                       # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认)
    SESSION_COOKIE_PATH = "/"                               # Session的cookie保存的路径(默认)
    SESSION_COOKIE_DOMAIN = None                             # Session的cookie保存的域名(默认)
    SESSION_COOKIE_SECURE = False                            # 是否Https传输cookie(默认)
    SESSION_COOKIE_HTTPONLY = True                           # 是否Session的cookie只支持http传输(默认)
    SESSION_COOKIE_AGE = 1209600                             # Session的cookie失效日期(2周)(默认)
    SESSION_EXPIRE_AT_BROWSER_CLOSE = False                  # 是否关闭浏览器使得Session过期(默认)
    # set_cookie('k1', 123)     # 没有设置超时时间,关闭浏览器就失效
    SESSION_SAVE_EVERY_REQUEST = False                       # 是否每次请求都保存Session,默认修改之后才保存(默认)
    # 使用 request.session.set_expiry(value) 设置了超时时间,但 SESSION_SAVE_EVERY_REQUEST = True时,刷新页面
    # 后的超时时间将根据当前刷新的时间重新设置。

- session依赖于cookie
-原理:随机字符串
-服务器session
    request.session.get()
    request.session[x] = x
    request.session.clear()

- 配置文件中设置默认操作,就是上面这些默认操作。

在 settings.py文件中添加下面这一句,默认将session放在数据库:
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
Django提供了5种类型的Session保存方法供使用:
(1)、数据库(默认)
(2)、缓存
(3)、文件
(4)、缓存+数据库
(5)、加密cookie

3、 session保存在缓存中的设置方法
session默认中保存在数据库中,可以对默认的保存方法进行修改,下面的代码是添加在 michael_04\settings.py 文件中:

SESSION_ENGINE = 'django.contrib.sessions.backends.cache'  # 缓存引擎
SESSION_CACHE_ALIAS = 'default'     # 这一句表示默认保存在哪里,default 指向下面CACHES中的default
CACHES = {
    'default': {        # 在这里指定了使用什么缓存,以及缓存的地址及端口
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': [
            '172.19.26.240:11211',
            '172.19.26.242:11211',
        ]
    }
}
例如 SESSION_CACHE_ALIAS = 'db1' 表示指向db1缓存,这时在CACHES中需要指定db1缓存的相关信息,例如下面这样:
CACHES = {
    'default': {        # 在这里指定了使用什么缓存,以及缓存的地址及端口
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': [
            '172.19.26.240:11211',
            '172.19.26.242:11211',
        ]
    }
    'db1': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': [
            '172.19.26.240:11211',
            '172.19.26.242:11211',
        ]
    }
}
要注意Django默认不支持Redis缓存,默认只支持 memcache ,要设置支持 Redis需要另外的插件

4、 session保存在文件中的设置方法
session还可以保存在文件中,要设置session保存在文件中,可以在 michael_04\settings.py 文件中做下面的设置:

SESSION_ENGINE = 'django.contrib.sessions.backends.file'    # 引擎
SESSION_FILE_PATH = None                                    # 缓存文件路径,如果为None,则使用tempfile模块获取一个临时地址tempfile.gettempdir()     # 如:/var/folders/d3/j9tj0gz93dg06bmwxmhh6_xm0000gn/T
SESSION_FILE_PATH = os.path.join(BASE_DIR, 'cache')     # 这样表示缓存文件路径是项目文件下的cache目录下

5、 session保存在缓存+数据库中
数据库用于做持久化,缓存用于提高效率,设置方式是在 settings.py 文件中添加下面这行代码:
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # 引擎
另外的设置方法同上。

6、 加密 cookie Session
设置加密,在 settings.py 文件中添加下面这行代码:
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' # 引擎

在上面这些配置中,Session的通用配置是:

SESSION_COOKIE_NAME  "sessionid"                       # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认)
SESSION_COOKIE_PATH  "/"                               # Session的cookie保存的路径(默认)
SESSION_COOKIE_DOMAIN = None                             # Session的cookie保存的域名(默认)
SESSION_COOKIE_SECURE = False                            # 是否Https传输cookie(默认)
SESSION_COOKIE_HTTPONLY = True                           # 是否Session的cookie只支持http传输(默认)
SESSION_COOKIE_AGE = 1209600                             # Session的cookie失效日期(2周)(默认)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False                  # 是否关闭浏览器使得Session过期(默认)
# set_cookie('k1', 123)     # 没有设置超时时间,关闭浏览器就失效
SESSION_SAVE_EVERY_REQUEST = False                       # 是否每次请求都保存Session,默认修改之后才保存(默认)
# 使用 request.session.set_expiry(value) 设置了超时时间,但 SESSION_SAVE_EVERY_REQUEST = True时,刷新页面
# 后的超时时间将根据当前刷新的时间重新设置。

引擎的配置是下面这句:
SESSION_ENGINE = 'django.contrib.sessions.backends.db'   # 引擎(默认),将 session 放在数据库

7、 使用session让用户登录和注销
当用户输入正确的用户名和密码,在后台设置相应的session值并返回正确的index页面。在index页面添加注销功能,点击注销后,后台清空该用户的所有session值。并返回到登录页面。在实现这个功能之前,先在 michael_04\urls.py 中添加下面这个URL:
path('logout/', views.logout),
接下来在 app01\views.py 文件中index()、login()、logout() 函数的代码如下所示:

def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == "POST":
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        if user == 'root' and pwd == '123':
            request.session['username'] = user
            request.session['is_login'] = True
            return redirect("/index/")
        else:
            return render(request, 'login.html')

def index(request):
    # 获取当前用户的随机字符串
    # 根据随机字符串获取对应信息
    if request.session.get('is_login', None):
        return render(request, 'index.html', {"username": request.session['username']})
    else:
        return HttpResponse('未登录')

def logout(request):
    # del request.session["username"]
    request.session.clear()     # 清空 session,所有的都清空
    return redirect('/login/')

在这段代码中,index()函数的条件判断改为使用 get()方法获取session值,在return语句中,render语句的返回值中的字典:
{"username": request.session['username']}
这样在前端可用 username 获取session 值,不过在返回值中还有 request 这个变量,可通过这个变量直接在前端获取session值也是一样的,获取方法是:request.session.username。在 logout() 函数,只要用户在index页面点击注销,就执行 logout()函数,在这个函数中清空该用户的所有 session 值,并重定向到 login 页面。index.html的代码如下所示:
文件位置:templates\index.html

<body>
    <h1>欢迎登录,{{ username }}, {{ request.session.username }}</h1>
    <a href="/logout/">注销</a>
</body>

在这段index.html代码,展示两种获取 session值的方法。

8、 session的超时时间
在Django中默认的session超时时间是2周,在后台可以使用这个 request.session.set_expiry(value) 方法设置session的超时时间,这个是优先级高于默认的超时时间。例如在 login() 函数中设置超时时间,详细代码如下所示:
app01\views.py 中的 login() 函数:

def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == "POST":
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        if user == 'root' and pwd == '123':
            request.session['username'] = user
            request.session['is_login'] = True
            if request.POST.get('rmb', None):
                request.session.set_expiry(10)      # 设置超时时间,单位是秒,这样设置所有用户都是这个超时时间
            return redirect("/index/")
        else:
            return render(request, 'login.html')

在 login.html文件增加一行代码 <input type="checkbox" name="rmb" value="1" />10秒钟免登录,这样在 login 页面勾选这个选项登录后,10秒过后就会重复返回登录页面。修改后的 login.html文件代码如下所示:

<body>
    <form action="/login/" method="POST">
        <input type="text" name="user" />
        <input type="text" name="pwd" />
        <input type="checkbox" name="rmb" value="1" />10秒钟免登录
        <input type="submit" value="提交" />
    </form>
</body>

二、 CSRF

1、 CSRF原理
CSRF是一个中间件,get 方式请求网站的时候都能正常显示,但以 post 方式提交时,用户名和密码不正确时,提交数据就会出现403页面,这是因为以 post提交数据,后台需要获取一个随机字符串,以判断提交数据是否合法。但是在提交的form标签中加上 {{ csrf_token }}后,刷新(login.html)登录页面后就可以在页面上看到一个随机字符串。以 {% csrf_token %} 方式添加在form标签中时,就会以隐藏的方式添加一个随机字符串,在页面上不显示,但是HTML源代码中可以看到这个随机字符串。这样在输入不正确的用户和密码提交时,不会出现403页面,而定位到登录页面。

在form表单提交时,后台需要获取这个CSRF随机字符串,可以通过 Ajax方式把这个随机字符串放在请求头中发送到后台。loing()函数的代码修改如下:

def login(request):
    # from django.conf import settings    # 这里的settings不是settings.py,要比 settings.py 中东西多很多
    # print(settings.CSRF_HEADER_NAME)    # 输出:HTTP_X_CSRFTOKEN

    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == "POST":
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        if user == 'root' and pwd == '123':
            request.session['username'] = user
            request.session['is_login'] = True
            if request.POST.get('rmb', None):
                request.session.set_expiry(10)      # 设置超时时间,单位是秒,这样设置所有用户都是这个超时时间
            return redirect("/index/")
        else:
            return render(request, 'login.html')

login.html文件的代码修改如下:

<body>
    <form action="/login/" method="POST">
        {% csrf_token %}
        <input type="text" name="user" />
        <input type="text" name="pwd" />
        <input type="checkbox" name="rmb" value="1" />10秒钟免登录
        <input type="submit" value="提交" />
        <input  id="btn" type="button" value="按钮" />
    </form>
    <script src="/static/jquery1.12.4.js"></script>
    <script src="/static/jquery.cookie-v1.4.1.js"></script>
    <SCRIPT>
        $(function () {
            //var csrftoken = $.cookie('csrftoken');
            $("#btn").click(function () {
                $.ajax({
                    url: '/login/',
                    type: 'POST',
                    data: {'user': 'root', 'pwd': 123},
                    // 加上headers后,不用注释掉settings.py 中CSRF行,没有正确的用户名和密码同样可以通过Ajax正常提交
                    headers: {'X-CSRFtoken': $.cookie('csrftoken')},
                    success: function (arg) {

                    }
                })
            })
        })
    </SCRIPT>
</body>

在HTML中的JS代码中,ajax发送时,添加了 headers字典,键是 X-CSRFtoken,值是获取到 cookie,这样发送到后台,在settings.py文件中不用注释掉 CSRF行也可以正常提交。

xhr:是xml http request 请求对象,所有 Ajax请求的底层都是用它来做的。
当HTML代码中有很多 Ajax请求时,每个请求都有 headers 这个请求头,这时可以对Ajax请求进行统一的设置,不用在每一个Ajax请求中都添加headers这个请求头。例如下面这段修改后的 login.html 文件代码所示:

<body>
    <form action="/login/" method="POST">
        {% csrf_token %}
        <input type="text" name="user" />
        <input type="text" name="pwd" />
        <input type="checkbox" name="rmb" value="1" />10秒钟免登录
        <input type="submit" value="提交" />
        <input  id="btn1" type="button" value="按钮1" />
        <input  id="btn2" type="button" value="按钮2" />
    </form>
    <script src="/static/jquery1.12.4.js"></script>
    <script src="/static/jquery.cookie-v1.4.1.js"></script>
    <SCRIPT>
        $(function () {
            $.ajaxSetup({
                beforeSend: function (xhr, settings) {
                    xhr.setRequestHeader('X-CSRFtoken', $.cookie('csrftoken'));
                }
            });
            $("#btn1,#btn2").click(function () {
                $.ajax({
                    url: '/login/',
                    type: 'POST',
                    data: {'user': 'root', 'pwd': 123},
                    // 加上headers后,不用注释掉settings.py 中CSRF行,没有正确的用户名和密码同样可以通过Ajax正常提交
                    //headers: {'X-CSRFtoken': $.cookie('csrftoken')},
                    success: function (arg) {
                    }
                })
            })
        })
    </SCRIPT>
</body>

这段代码中有两个按钮,是按钮1和按钮2,都是通过 Ajax请求提交,这时可以通过 $.ajaxSetup() 进行统一Ajax设置。不用在每一个Ajax请求都写headers这个请求头。

2、 CSRF的设置方法
在settings.py文件中取消注释掉CSRF行后,Django默认所有发送的 POST请求都要带上 csrftoken 才能提交,这就是全局设置。这样假设有上百个 views.py 模块,其中有2个不加CSRF,或者只有2个才要加CSRF,此时使用全局的设置就有些不恰当。

django为用户实现防止跨站请求伪造的功能,通过中间件 django.middleware.csrf.CsrfViewMiddleware 来完成。而对于django中设置防跨站请求伪造功能有分为全局和局部。

在settings.py文件中的CSRF行所在的变量名是 MIDDLEWARE,这就是中间件。这里的中间件应用上后,表示全局都要应用上它的功能。

此外,还有局部设置的方式,通过导入 csrf_exempt, csrf_protect 两个模块来实现,导入方法是:
from django.views.decorators.csrf import csrf_exempt,csrf_protect

局部装饰时使用方法,装饰views中的函数:
@csrf_protect,为当前函数强制设置防跨站请求伪造功能,即便settings中没有设置全局中间件。
@csrf_exempt,取消当前函数防跨站请求伪造功能,即便settings中设置了全局中间件。

另外在HTML中,Ajax提交时,如果是POST方式提交要带上csrftoken,如果以其它方式提交,则不需要带上csrftoken,这时JS的代码可以修改为如下形式:

var csrftoken = $.cookie('csrftoken');
function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        }
    }
});

三、 中间件

中间件非常重要。在 settings.py 文件中的 MIDDLEWARE 就是中间件。当一个请求过来的时候,首先到达中间件,经过每一个中间件后再到达 views中的函数,views中的函数返回的时候也是通过中间最后面的中间件往上一层一层传递到用户那里。如果中间件是一个类,那么 MIDDLEWARE 的中间件就是类的方法。

当请求来的时候,是通过 process_request 在中间件中传递到 views函数,views函数又通过 process_response 在中间件中传递
到用户。如下图1-2所示。

图1-2 中间件传递过程


这个中间件是Django提供的,也可以自己做一个中间件来使用。要自己做中间件,需要遵循一些规则才行。通过查看 csrf的源码可以发现,csrf定义的类是 class CsrfViewMiddleware(MiddlewareMixin),这个类继承了 MiddlewareMixin,如果自己写中间件也要继承这个类。另外 csrf 这个类名是 CsrfViewMiddleware,其实这个类名是路径。下面来自己做一个中间件。

1、 中间件中process_request()函数执行顺序,process_response()函数的返回顺序

首先在项目文件夹michael_04下创建文件夹 Middle,在 Middle 文件夹下创建文件 m1.py,在这个 m1.py 文件中写类。代码如下所示:
位置:michael_04\Middle\m1.py

from django.utils.deprecation import MiddlewareMixin

class Row1(MiddlewareMixin):
    def process_request(self, request):
        print("row1")

class Row2(MiddlewareMixin):
    def process_request(self, request):
        print("row2")

class Row3(MiddlewareMixin):
    def process_request(self, request):
        print("row3")

还需要在 settings.py 文件中的 MIDDLEWARE 变量中添加自定义的中间件,添加后的 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',
    'Middle.m1.Row1',
    'Middle.m1.Row2',
    'Middle.m1.Row3',
]

为了测试自定义的中间件,先在 urls.py 文件中添加下面这个测试 URL:
path('test/', views.test),
在app01\views.py 文件写 test() 函数代码如下所示:

位置:michael_04\Middle\m1.py

class Row1(MiddlewareMixin):
    def process_request(self, request):
        print("row1")
    def process_response(self, request, response):
        print("line1")
        return response

class Row2(MiddlewareMixin):
    def process_request(self, request):
        print("row2")
    def process_response(self, request, response):
        print("line2")
        return response

class Row3(MiddlewareMixin):
    def process_request(self, request):
        print("row3")
    def process_response(self, request, response):
        print("line3")
        return response

在这段代码中,主要是在3个类中添加了 process_response() 方法,另外还传递了两个参数 request和response,这里定义的方法名称和参数传递的顺序都不能变。此时在刷新 127.0.0.1:8000/test/ 页面,此时后台的输出顺序如下图1-3所示:

图1-3 中间件对请求的处理顺序

在 process_response() 方法中的request与views.py中函数的request参数是一样,使用方法也是一样的。

现在假设 Row2 这个类中的 process_request() 方法有 return 语句,那么请求就不会传递到下一个中间件,而是到了这个return语句后,就返回到上一个中间件了。例如将 Row2这个类的代码改写如下:

class Row2(MiddlewareMixin):
    def process_request(self, request):
        print("row2")
        return HttpResponse("成都")   # 增加这条 return 语句
    def process_response(self, request, response):
        print("line2")
        return response

此时再刷新 127.0.0.1:8000/test/ 页面,页面上显示成都,后台输出是 row1、row2、line2、line1。这是Django 1.10之后的版本是这样返回的,1.10之前的版本不是这样的,而是到达最后一个中间件再返回,不到 views中的函数。

中间件适合对所有的请求做统一的操作。比如黑名单过滤。

2、 中间件中process_view()函数的执行顺序
在自定义中间件中除了process_request()函数和process_response()函数外,还有process_view()函数。这个process_view()函数在中间件中又是怎样的一个执行顺序呢。先来看下面这段代码,这段代码是m1.py文件中的Row1、Row2、Row3类代码:
位置:michael_04\Middle\m1.py

class Row1(MiddlewareMixin):
    def process_request(self, request):
        print("row1")
    def process_view(self, request, view_func, view_func_args, view_func_kwargs):
        print("四川成都")
    def process_response(self, request, response):
        print("line1")
        return response

class Row2(MiddlewareMixin):
    def process_request(self, request):
        print("row2")
    def process_view(self, request, view_func, view_func_args, view_func_kwargs):
        print("湖北武汉")
    def process_response(self, request, response):
        print("line2")
        return response

class Row3(MiddlewareMixin):
    def process_request(self, request):
        print("row3")
    def process_view(self, request, view_func, view_func_args, view_func_kwargs):
        print("中国上海")
    def process_response(self, request, response):
        print("line3")
        return response

再次刷新 127.0.0.1:8000/test/ 页面,在后台可以看到如图1-4的输出顺序,由图中的输出顺序可知,在中间件中函数的执行顺序是 Row1.process_request()、Row2.process_request()、Row3.process_request(),紧接着又执行 Row1.process_view()、Row2.process_view()、Row3.process_view(),接下来才执行 views.py中的test()函数,接着再执行自定义中间件中的Row3.process_response()、Row2.process_response()、Row1.process_response()。要对这个执行顺序理解清楚。

图1-4 自定义中间件中各方法执行结果

在中间件中的 process_view(self, request, view_func, view_func_args, view_func_kwargs) 函数中的参数含义:request与views中函数的request参数是一样的,view_func参数是views中的函数,view_func_args是views中函数的参数,view_func_kwargs也是views中函数的参数。在views中的函数如果是位置参数就放到view_func_args参数中,如果是关键字参数就放到view_func_kwargs参数中。

3、 中间件中process_exception()异常处理函数
这个process_exception()函数是做异常处理用的,是当views.py中的函数出现异常的时候才会执行这个函数。函数定义方法如下:

def process_exception(self, request, exception):
    print("出现异常信息")

要注意这个函数是定义在中间件的类中,这个函数中的exception就是views.py中的函数产生的异常信息。例如要在这个函数中处理一个在views.py中函数产生的ValueError的异常信息,这个process_exception()函数可这样做:

def process_exception(self, request, exception):
    if isinstance(exception, ValueError):
        return HttpResponse("出现异常信息")

这样当views.py中的函数出现这个ValueError异常时,页面上就会看到"出现异常信息",这样避免在页面上看到一堆的错误代码。

如果在中间件中的每个类中都定义了异常处理函数,则当views中的函数产生异常时,先从最下面的类中定义异常处理函数开始执行,如果异常处理函数没能处理掉异常,则继续往一层找,如果正确处理了异常信息,则返回到最下层的process_response()函数开始一层一层的拄上执行。如果所有类中的异常处理函数都不能正确处理异常,则页面直接就报错了。

4、 中间件中process_template_response()函数
这个process_template_response()函数在什么情况下会执行?在 如果 Views中的函数返回的对象中,具有render方法时就会执行。例如在views中定义一个类Foo,在这个类中定义一个 render方法,在test函数中返回这个类实例,代码如下所示:

class Foo:
    def render(self):
        return HttpResponse('OK')

def test(request):
    print("hello world!")
    return Foo()

这样定义后,在中间件的Row3类定义一个process_template_response()函数,Row3类的完整代码如下所示:

class Row3(MiddlewareMixin):
    def process_request(self, request):
        print("row3")

    def process_view(self, request, view_func, view_func_args, view_func_kwargs):
        print("中国上海")

    def process_response(self, request, response):
        print("line3")
        return response

    def process_exception(self, request, exception):
        if isinstance(exception, ValueError):
            return HttpResponse("出现异常信息")

    def process_template_response(self, request, response):
        # 如果 Views中的函数返回扔对象中,具有render方法就会执行这个函数
        print("执行process_template_response()函数")
        return response

此时刷新 127.0.0.1:8000/test/ 页面,在后台的输出中就可以看到process_template_response()函数被执行。另外在这个process_template_response()函数中,如果没有 return response ,在页面上会显示出错的代码信息。对于这个process_template_response()函数在实际中用得比较少。

在中间件中必须要会的三个函数是 process_request()、process_view()、process_response()。

(下一篇博文继续本节内容)