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的代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
<!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>
完成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开始计算 |