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>
完成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开始计算 |
来源:oschina
链接:https://my.oschina.net/u/4258221/blog/3381321