玩转Django2.0---Django笔记建站基础十(二)(常用的Web应用程序)

10.3  CSRF防护

  CSRF(跨站请求伪造)也成为One Click Attack或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用,窃取网站的用户信息来制作恶意请求。

  Django为了防护这类攻击,在用户提交表单时,表单会自动加入csrftoken的隐含值,这个隐含值会与网站后台保存的csrftoken进行匹配,只有匹配成功,网站才会处理表单数据。这种防护机制称为CSRF防护,原理如下:

    1、在用户访问网站时,Django在网页的表单中生成一个隐含字段csrftoken,这个值是在服务器端随机生成的。

    2、当用户提交表单时,服务器检验表单的csrftoken是否和自己保存的csrftoken一致,用来判断当前请求是否合法。

    3、如果用户被CSRF攻击并从其他地方发生攻击请求,由于其他地方不可能知道隐藏的csrftoken信息,因此导致网站后台校验csrftoken失败,攻击就被成功防御。

  在Django中使用CSRF防护功能,首先在配置文件settings.py中设置防护功能的配置信息。功能的开启由配置文件的中间件django.middleware.csrf.CsrfViewMiddleware实现,在创建项目时已默认开启,如下图:

 

设置CSRF防护

  CSRF防护只作用于POST请求,并不防护GET请求,因为GET请求以只读形式访问网站资源,并不破坏和篡改网站数据。以MyDjango为例,在模板user.html的表单<form>标签中加入内置标签csrf_token即可实现CSRF防护,代码如下:

#user/user.html部分代码
<div class="flex-center">
    <div class="container">
    <div class="flex-center">
    <div class="unit-1-2 unit-1-on-mobile">
        <h1>MyDjango Auth</h1>
            {% if tips %}
        <div>{{ tips }}</div>
            {% endif %}
        <form class="form" action="" method="post">
            {% csrf_token %}
            <div>用户名:<input type="text" name='username'></div>
            <div>密 码:<input type="password" name='password'></div>
            <button type="submit" class="btn btn-primary btn-block">确定</button>
        </form>
    </div>
    </div>
    </div>
</div>

  启动运行MyDjango,在浏览器中打开用户登录页面,然后查看页面的源码,可以发现表单新增隐藏域,隐藏域是由模板语法{%  csrf_token  %}所生成的,网站生成的csrftoken都会记录在隐藏域的value属性中。当用户每次提交表单时,csrftoken都会随之变化,如下图:

 

csrftoken信息

  如果想要取消表单的CSRF防护,可以在模板上删除{%  csrf_token  %},并且在相应的视图函数中添加装饰器@csrf_exempt,代码如下:

from django.views.decorators.csrf import csrf_exempt

#取消CSRF防护
@csrf_exempt
def registerView(request):
    pass
    return render(request, 'user.html', locals())

  如果只是在模板上删除{%  csrf_token  %},并没有在相应的视图函数中设置过滤器@csrf_exempt,那么当用户提交表单时,程序因CSRF验证失败而抛出403异常的页面,如下图:

 

CSRF验证失败

  最后还有一种比较特殊的情况,如果在配置文件settings.py中删除中间件CsrfViewMiddleware,这样使整个网站都取消CSRF防护。在全站没有CSRF防护的情况下,又想对某些请求设置CSRF防护,那么在模板上添加模板语法{%  csrf_token  %},然后在相应的视图函数中添加装饰器@csrf_protect即可实现,代码如下:

from django.views.decorators.csrf import csrf_protect, csrf_exempt

#添加CSRF防护
@csrf_protect
def registerView(request):
    pass
    return render(request, 'user.html', locals())

  值得注意的是,在日常开发中,如果网页某些数据时使用前端的Ajax实现表单提交的,那么Ajax向服务器发送POST请求时,请求参数必须添加csrftoken的信息,否则服务器会视该请求是恶意请求。实现代码如下:

<script>
    function submitForm() {
        var csrf = $('input[name="csrfmiddlewaretoken"]').val();
        var user = $('#user').val();
        var password = $('#password').val();
        $.ajax({
            url: '/csrf1.html',
            type: 'POST',
            data: {'user': user,
                   'password': password,
                   'csrfmiddlewaretoken': csrf,}
            success: function (arg) {
                console.log(arg);
            }
        })
    }
</script>

 

10.4  消息提示

  在网页应用中,当处理完表单或完成其他信息输入后,网站会有相应的操作提示。Django有内置消息提示功能供开发者直接使用,信息提示功能由中间件SessionMiddleware、MessageMiddleware和INSTALLED_APPS的django.contrib.messages共同实现。在创建Django项目时,消息提示功能已默认开启,如下图:

 

消息提示功能配置

  消息提示必须依赖中间件SessionMiddleware,因为消息提示的引擎默认是SessionStorage,而SessionStorage是在Session的基础上实现的,同时说明了中间件SessionMiddleware为什么设置在MessageMiddleware的前面。

  使用信息提示功能之前,需要了解消息提示的类型,Django提供了5种消息类型,说明如下表所示:

类型 说明
DEBUG 提示开发过程中的相关调式信息
INFO 提示信息,如用户信息
SUCCESS 提示当前操作执行成功
WARNING 警告当前操作存在风险
ERROR 提示当前操作错误

Django提供的5种消息类型

  若想在开发中使用消息提示,首先在视图函数中生成相关的信息内容,然后在模板中将信息内容展现在网页上。因此,在index中定义相关的URL地址和相应的视图函数,代码如下:

from django.urls import path
from . import views
urlpatterns = [
    # 首页的URL
    path('', views.index, name='index'),
    # 购物车
    path('ShoppingCar.html', views.ShoppingCarView, name='ShoppingCar'),
    # 消息提示
    path('message.html', views.messageView, name='message'),
]

#index/index.html
from django.template import RequestContext
from django.contrib import messages

# 消息提示
def messageView(request):
    # 信息添加方法一
    messages.info(request, '信息提示')
    messages.success(request, '信息正确')
    messages.warning(request, '信息警告')
    messages.error(request, '信息错误')
    # 信息添加方法二
    messages.add_message(request, messages.INFO, '信息提示')
    return render(request, 'message.html', locals(), RequestContext(request))

  在视图函数messageView中可以看到添加信息有两种方式,两者实现的功能是一样的。在函数返回时,必须设置RequestContext(request),这是Django的上下文处理器,确保信息messages对象能在模板中使用。最后在index的template中创建模板message.html,模板代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>信息提示</title>
</head>
<body>
    {% if messages %}
        <ul>
            {% for message in messages %}
                {# message.tags代表信息类型 #}
                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
    {% else %}
        <script>alert('暂无信息');</script>
    {% endif %}
</body>
</html>

  在上述例子中,视图函数messageView将对象messages通过上下文处理器RequestContext(request)传递给模板变量messages,然后将模板变量messages的内容遍历输出,最后通过模板引擎解析生成HTML网页。在浏览器上访问http://127.0.0.1:8000/message.html,网页信息如下图:

 

消息提示功能应用

 

 

10.5  分页功能

  在网页上浏览数据的时候,数据列表的下方都能看到翻页功能,而且每一页的数据都不相同。比如在淘宝上搜索某商品的关键字,淘宝会根据用户提供的关键字返回符合条件的商品信息,并且对这些商品信息进行分页处理,用户可以在商品信息的下方单击相应的页数按钮查看。

  如果要实现数据的分页功能,需要考虑多方面因素:

    1、当前用户访问的页数是否存在上(下)一页。

    2、访问的页数是否超出页数上限。

    3、数据如何按页截取,如何设置每页的数据量

  Django已为开发者提供了内置的分页功能,开发者无须自己实现数据分页功能,只需调用Django内置分页功能的函数即可实现。在实现网站数据分页之前,首先了解Django的分页功能为开发者提供了那些方法与函数,在PyCharm的Terminal中开启Django的shell模式,函数使用说明如下:

(py3_3) E:\test5\MyDjango>python manage.py shell

#导入分页功能模块
In [1]: from django.core.paginator import Paginator
#生成数据列表
In [2]: objects = [chr(x) for x in range(97,107)]

In [3]: objects
Out[3]: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
#将数据列表以每三个元素分为一页
In [4]: p = Paginator(objects, 3)
#输出全部数据,即整个数据列表
In [5]: p.object_list
Out[5]: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
#获取数据列表的长度
In [6]: p.count
Out[6]: 10

#分页后的总页数
In [7]: p.num_pages
Out[7]: 4
#将页数转换成range循环对象
In [8]: p.page_range
Out[8]: range(1, 5)
#获取第二页的数据信息
In [9]: page2 = p.page(2)
#判断第二页是否存在上一页
In [10]: page2.has_previous()
Out[10]: True
#如果当前页数存在上一页,就输出上一页的页数,否则抛出EmptyPage异常
In [11]: page2.previous_page_number()
Out[11]: 1
#判断第二页是否存在下一页
In [12]: page2.has_next()
Out[12]: True

#如果当前页数存在下一页,就输出下一页的页数,否则抛出EmptyPage异常
In [13]: page2.next_page_number()
Out[13]: 3
#输出第二页所对应的数据内容
In [14]: page2.object_list
Out[14]: ['d', 'e', 'f']
#输出第二页的第一条数据在整个数据列表的位置,数据位置从1开始计算
In [15]: page2.start_index()
Out[15]: 4
#输出第二页的最后一条数据在整个数据列表的位置,数据位置从1开始计算
In [16]: page2.end_index()
Out[16]: 6

  上述代码是Django分页功能的使用方法,根据对象类型可以将代码分为两部分:分页对象p和某分页对象page2,两者说明如下:

    (1)分页对象p:由模块Paginator实例化生成。在Paginator实例化时,需要传入参数object和per_page,参数object是待分页的数据对象,参数per_page用于设置每页的数据量。对象p提供表所示的函数:

函数 说明
object_list 输出被分页的全部数据,即数据列表objects
Count 获取当前被分页的数据总量,即数据列表objects的长度
num_pages 获取分页后的总页数
page_range 将总页数转换成range循环对象
page(number) 获取某一页的数据对象,参数number代表页数

     (2)某分页对象page2:由对象p使用函数page所生成的对象。page2提供表所示的函数:

函数 说明
has_previous() 判断当前页数是否存在上一页
previous_page_number() 如果当前页数存在上一页,输出上一页的页数,否则抛出EmptyPage异常
has_next() 判断当前页数是否存在下一页
next_page_number() 如果当前页数存在下一页,输出下一页的页数,否则抛出EmptyPage异常
object_list 输出当前分页的数据信息
start_index() 输出当前分页的第一条数据在整个数据列表的位置,数据位置以1开始计算
end_index() 输出当前分页的最后一条数据在整个数据列表的位置,数据位置以1开始计算

  我们通过一个示例来讲述如何在开发过程中使用Django内置分页功能。在MyDjango的数据库中分别对数据表index_type和index_product添加数据信息,数据来源于第6章,可以在本书源码中找到数据文件,如下图所示:

 

   然后在index中添加模板pagination.html,模板的样式文件common.css和pagination.css存放在index的静态文件夹static中,如下图:

 

  完成项目的环境搭建后,本示例的分页功能需要由index的urls.py、views.py和pagination.html共同实现,首先在urls.py和views.py中分别添加以下代码:

#index/urls.py
from django.urls import path
from . import views

urlpatterns = [
    # 首页
    path('', views.index, name='index'),
    # 购物车
    path('ShoppingCar.html', views.ShoppingCarView, name='ShoppingCar'),
    # 分页功能
    path('pagination/<int:page>.html', views.paginationView, name='pagination'),
]

#index/views.py
#views.py的paginationView函数
#导入paginator模块
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
#分页功能
def paginationView(request, page):
    # 获取数据表index_product全部数据
    Product_list = Product.objects.all()
    # 设置每一页的数据量为3
    paginator = Paginator(Product_list, 3)
    try:
        pageInfo = paginator.page(page)
    except PageNotAnInteger:
        # 如果参数page的数据类型不是整型,则返回第一页数据
        pageInfo = paginator.page(1)
    except EmptyPage:
        # 用户访问的页数大于实际页数,则返回最后一页的数据
        pageInfo = paginator.page(paginator.num_pages)
    return render(request, 'pagination.html', locals())

  上述代码设置了分页功能的URL地址和相应的视图函数,其说明如下:

    1、URL地址设置了动态变量page,该变量代表用户当前访问的页数。

    2、函数paginationView首先获取数据表index_product中的全部数据,生成变量Product_list。

    3、通过分页模块paginator对变量Product_list进行分页,以每3条数据划分为一页。

    4、使用函数page获取分页对象paginator中某一页分页的数据信息。

    5、当变量page传入函数page时,如果变量page不是整型,程序就会抛出PageNotAnInteger异常,然后返回第一页的数据。

    5、如果变量page的数值大于总页数,程序就会抛出EmptyPage异常,然后返回最后一页的数据。异常PageNotAnInteger和EmptyPage都来自于模块paginator。

  将视图函数处理的结果传给模板文件pagination.html,然后把分页后的数据展示在网页中。模板文件pagination.html的代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>分页功能</title>
    {# 导入CSS样式文件 #}
    {% load staticfiles %}
    <link type="text/css" rel="stylesheet" href="{% static "css/common.css" %}">
    <link type="text/css" rel="stylesheet" href="{% static "css/pagination.css" %}">
</head>
<body>
<div class="wrapper clearfix" id="wrapper">
<div class="mod_songlist">
    <ul class="songlist__header">
        <li class="songlist__header_name">产品名称</li>
        <li class="songlist__header_author">重量</li>
        <li class="songlist__header_album">尺寸</li>
        <li class="songlist__header_other">产品类型</li>
    </ul>
    <ul class="songlist__list">
        {# 列出当前分页所对应的数据内容 #}
        {% for item in pageInfo %}
        <li class="js_songlist__child" mid="1425301" ix="6">
            <div class="songlist__item">
                <div class="songlist__songname">{{item.name}}</div>
                <div class="songlist__artist">{{item.weight}}</div>
                <div class="songlist__album">{{item.size}}</div>
                <div class="songlist__other">{{ item.type }}</div>
            </div>
        </li>
        {% endfor %}
    </ul>
    {# 分页导航 #}
    <div class="page-box">
    <div class="pagebar" id="pageBar">
    {# 上一页的URL地址 #}
    {% if pageInfo.has_previous %}
        <a href="{% url 'pagination' pageInfo.previous_page_number %}" class="prev"><i></i>上一页</a>
    {% endif %}
    {# 列出所有的URL地址 #}
    {% for num in pageInfo.paginator.page_range %}
        {% if num == pageInfo.number %}
            <span class="sel">{{ pageInfo.number }}</span>
        {% else %}
            <a href="{% url 'pagination' num %}" target="_self">{{num}}</a>
        {% endif %}
    {% endfor %}
    {# 下一页的URL地址 #}
    {% if pageInfo.has_next %}
        <a href="{% url 'pagination' pageInfo.next_page_number %}" class="next">下一页<i></i></a>
    {% endif %}
    </div>
    </div>
</div><!--end mod_songlist-->
</div><!--end wrapper-->
</body>
</html>
pagination.html

  完成urls.py、views.py和pagination.html的代码编写后,最后测试功能是否正常运行。启动项目并在浏览器上访问http://127.0.0.1:8000/pagination/1.html,单击分页导航时,程序会字段跳转到相应的URL地址并返回对应的数据信息,运行结果如下图:

 

 

 

10.6  本章小结

  Django为开发者提供了常见的Web应用程序,如会话控制、高速缓存、CSRG防护、消息提示和分页功能。内置的Web应用程序大大优化了网站性能,并且完善了安全防护机制,而且也提高了开发者的开发效率。

  Django提供5种不同的缓存方式,每种缓存方式说明如下:

    1、Memcached:一个高性能的分布式内存对象缓存系统,用于动态网站,以减轻数据库负载。通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高网站的响应速度。使用Memcached需要安装系统服务器,Django通过python-memcached或pylibmc模块调用Memcached系统服务器,实现缓存读写操作,适合超大型网站使用。

    2、数据库缓存:缓存信息存储在网站数据库的缓存表中,缓存表可以在项目的配置文件中配置,适合大中型网站使用。

    3、文件系统缓存:缓存信息以文本文件格式保存,适合中小型网站使用。

    4、本地内存缓存:Django默认的缓存保存方式,将缓存存放在项目所在系统的内存中,只适用于项目开发测试。

    5、虚拟缓存:Django内置的虚拟缓存,实际上只提供缓存接口,并不能存储缓存数据,只适用于项目开发测试。

  Django为了防护这类攻击,在用户提交表单时,表单会自动加入csrftoken的隐含值,这个隐含值会与网站后台保存的csrftoken进行匹配,只有匹配成功,网站才会处理表单数据。这种防护机制称为CSRF防护,原理如下:

    1、在用户访问网站时,Django在网页的表单中生成一个隐含字段csrftoken,这个值是在服务器端随机生成的。

    2、当用户提交表单时,服务器检验表单的csrftoken是否和自己保存的csrftoken一致,用来判断当前请求是否合法。

    3、如果用户被CSRF攻击并从其他地方发生攻击请求,由于其他地方不可能知道隐藏的csrftoken信息,因此导致网站后台校验csrftoken失败,攻击就被成功防御。

  消息提示必须依赖中间件SessionMiddleware,因为消息提示的引擎默认是SessionStorage,而SessionStorage是在Session的基础上实现的,同时说明了中间件SessionMiddleware为什么设置在MessageMiddleware的前面。

  使用信息提示功能之前,需要了解消息提示的类型,Django提供了5种消息类型,说明如下表所示:

类型 说明
DEBUG 提示开发过程中的相关调式信息
INFO 提示信息,如用户信息
SUCCESS 提示当前操作执行成功
WARNING 警告当前操作存在风险
ERROR 提示当前操作错误

Django提供的5种消息类型

上述代码是Django分页功能的使用方法,根据对象类型可以将代码分为两部分:分页对象p和某分页对象page2,两者说明如下: 

函数 说明
object_list 输出被分页的全部数据,即数据列表objects
Count 获取当前被分页的数据总量,即数据列表objects的长度
num_pages 获取分页后的总页数
page_range 将总页数转换成range循环对象
page(number) 获取某一页的数据对象,参数number代表页数

 

函数 说明
has_previous() 判断当前页数是否存在上一页
previous_page_number() 如果当前页数存在上一页,输出上一页的页数,否则抛出EmptyPage异常
has_next() 判断当前页数是否存在下一页
next_page_number() 如果当前页数存在下一页,输出下一页的页数,否则抛出EmptyPage异常
object_list 输出当前分页的数据信息
start_index() 输出当前分页的第一条数据在整个数据列表的位置,数据位置以1开始计算
end_index() 输出当前分页的最后一条数据在整个数据列表的位置,数据位置以1开始计算

posted @ 2019-10-01 22:30  python坚持者  阅读(520)  评论(2编辑  收藏  举报