第十六章:集成的子框架 django.contrib

Python有众多优点,其中之一就是“开机即用”原则: 安装Python的同时会安装好大量的标准软件包,这样 你可以立即使用而不用自己去下载。 Django也遵循这个原则,它同样包含了自己的标准库。 这一章就来讲 这些集成的子框架。

Django标准库

Django的标准库存放在 django.contrib 包中。每个子包都是一个独立的附加功能包。 这些子包一般是互相独立的,不过有些django.contrib子包需要依赖其他子包。

django.contrib 中对函数的类型并没有强制要求 。其中一些包中带有模型(因此需要你在数据库中安装对应的数据表),但其它一些由独立的中间件及模板标签组成。

django.contrib 开发包共有的特性是: 就算你将整个django.contrib开发包删除,你依然可以使用 Django 的基础功能而不会遇到任何问题。 当 Django 开发者向框架增加新功能的时,他们会严格根据这一原则来决定是否把新功能放入django.contrib中。

django.contrib 由以下开发包组成:

  • admin : 自动化的站点管理工具。 请查看第6章。

  • admindocs:为Django admin站点提供自动文档。 本书没有介绍这方面的知识;详情请参阅Django官方文档。

  • auth : Django的用户验证框架。 参见第十四章。

  • comments : 一个评论应用,目前,这个应用正在紧张的开发中,因此在本书出版的时候还不能给出一个完整的说明,关于这个应用的更多信息请参见Django的官方网站. 本书没有介绍这方面的知识;详情请参阅Django官方文档。

  • contenttypes : 这是一个用于引入文档类型的框架,每个安装的Django模块作为一种独立的文档类型。 这个框架主要在Django内部被其他应用使用,它主要面向Django的高级开发者。 可以通过阅读源码来了解关于这个框架的更多信息,源码的位置在 django/contrib/contenttypes/

  • csrf : 这个模块用来防御跨站请求伪造(CSRF)。参 见后面标题为”CSRF 防御”的小节。

  • databrowse:帮助你浏览数据的Django应用。 本书没有介绍这方面的知识;详情请参阅Django官方文档。

  • flatpages : 一个在数据库中管理单一HTML内容的模块。 参见后面标题为“Flatpages”的小节。

  • formtools:一些列处理表单通用模式的高级库。 本书没有介绍这方面的知识;详情请参阅Django官方文档。

  • gis:为Django提供GIS(Geographic Information Systems)支持的扩展。 举个例子,它允许你的Django模型保存地理学数据并执行地理学查询。 这个库比较复杂,本书不详细介绍。 请参看http://geodjango.org/上的文档。

  • humanize : 一系列 Django 模块过滤器,用于增加数据的人性化。 参阅稍后的章节《人性化数据》。

  • localflavor:针对不同国家和文化的混杂代码段。 例如,它包含了验证美国的邮编 以及爱尔兰的身份证号的方法。

  • markup : 一系列的 Django 模板过滤器,用于实现一些常用标记语言。 参阅后续章节《标记过滤器》。

  • redirects : 用来管理重定向的框架。 参看后面的“重定向”小节。

  • sessions : Django 的会话框架。 参见14章。

  • sitemaps : 用来生成网站地图的 XML 文件的框架。 参见13章。

  • sites : 一个让你可以在同一个数据库与 Django 安装中管理多个网站的框架。 参见下一节:

  • syndication : 一个用 RSS 和 Atom 来生成聚合订阅源的的框架。 参见13章。

  • webdesign:对设计者非常有用的Django扩展。 到编写此文时,它只包含一个模板标签{% lorem %}。详情参阅Django文档。

本章接下来将详细描述前面没有介绍过的 django.contrib 开发包内容。

多个站点

Django 的多站点系统是一种通用框架,它让你可以在同一个数据库和同一个Django项目下操作多个网站。 这是一个抽象概念,理解起来可能有点困难,因此我们从几个让它能派上用场的实际情景入手。

情景1:多站点间复用数据

正如我们在第一章里所讲,Django 构建的网站 LJWorld.com 和 Lawrance.com 是用由同一个新闻组织控制的: 肯萨斯州劳伦斯市的 劳伦斯日报世界 报纸。 LJWorld.com 主要做新闻,而 Lawrence.com 关注本地娱乐。 然而有时,编辑可能需要把一篇文章发布到 两个 网站上。

解决此问题的死脑筋方法可能是使用每个站点分别使用不同的数据库,然后要求站点维护者把同一篇文章发布两次: 一次为 LJWorld.com,另一次为Lawrence.com。 但这对站点管理员来说是低效率的,而且为同一篇文章在数据库里保留多个副本也显得多余。

更好的解决方案? 两个网站用的是同一个文章数据库,并将每一篇文章与一个或多个站点用多对多关系关联起来。 Django 站点框架提供数据库表来记载哪些文章可以被关联。 它是一个把数据与一个或多个站点关联起来的钩子。

情景2:把网站的名字/域名保存在一个地方

LJWorld.com 和 Lawrence.com 都有邮件提醒功能,使读者注册后可以在新闻发生后立即收到通知。 这是一种完美的的机制: 某读者提交了注册表单,然后马上就受到一封内容是“感谢您的注册”的邮件。

把这个注册过程的代码实现两遍显然是低效、多余的,因此两个站点在后台使用相同的代码。 但感谢注册的通知在两个网站中需要不同。 通过使用 Site 对象,我们通过使用当前站点的 name (例如 'LJWorld.com' )和 domain (例如 'www.ljworld.com' )可以把感谢通知抽提出来。

Django 的多站点框架为你提供了一个位置来存储 Django 项目中每个站点的 namedomain ,这意味着你可以用同样的方法来重用这些值。

如何使用多站点框架

多站点框架与其说是一个框架,不如说是一系列约定。 所有的一切都基于两个简单的概念:

  • 位于 django.contrib.sitesSite 模型有 domainname 两个字段。

  • SITE_ID 设置指定了与特定配置文件相关联的 Site 对象之数据库 ID。

如何运用这两个概念由你决定,但 Django 是通过几个简单的约定自动使用的。

安装多站点应用要执行以下几个步骤:

  1. 'django.contrib.sites' 加入到 INSTALLED_APPS 中。

  1. 运行 manage.py syncdb 命令将 django_site 表安装到数据库中。 这样也会建立默认的站点对象,域名为 example.com。

  1. example.com改成你自己的域名,然后通过Django admin站点或Python API来添加其他Site对象。 为该 Django 项目支撑的每个站(或域)创建一个 Site 对象。

  1. 在每个设置文件中定义一个 SITE_ID 变量。 该变量值应当是该设置文件所支撑的站点Site 对象的数据库 ID。

多站点框架的功能

下面几节讲述的是用多站点框架能够完成的几项工作。

多个站点的数据重用

正如在情景一中所解释的,要在多个站点间重用数据,仅需在模型中为 Site 添加一个 多对多字段 即可,例如:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    sites = models.ManyToManyField(Site)

这是在数据库中为多个站点进行文章关联操作的基础步骤。 在适当的位置使用该技术,你可以在多个站点中重复使用同一段 Django 视图代码。 继续 Article 模型范例,下面是一个可能的 article_detail 视图:

from django.conf import settings
from django.shortcuts import get_object_or_404
from mysite.articles.models import Article

def article_detail(request, article_id):
    a = get_object_or_404(Article, id=article_id, sites__id=settings.SITE_ID)
    # ...

该视图方法是可重用的,因为它根据 SITE_ID 设置的值动态检查 articles 站点。

例如, LJWorld.coms 设置文件中有有个 SITE_ID 设置为 1 ,而 Lawrence.coms 设置文件中有个 SITE_ID 设置为 2 。如果该视图在 LJWorld.coms 处于激活状态时被调用,那么它将把查找范围局限于站点列表包括 LJWorld.com 在内的文章。

将内容与单一站点相关联

同样,你也可以使用 外键 在多对一关系中将一个模型关联到 Site 模型。

举例来说,如果某篇文章仅仅能够出现在一个站点上,你可以使用下面这样的模型:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    site = models.ForeignKey(Site)

这与前一节中介绍的一样有益。

从视图钩挂当前站点

在底层,通过在 Django 视图中使用多站点框架,你可以让视图根据调用站点不同而完成不同的工作,例如:

from django.conf import settings

def my_view(request):
    if settings.SITE_ID == 3:
        # Do something.
    else:
        # Do something else.

当然,像那样对站点 ID 进行硬编码是比较难看的。 略为简洁的完成方式是查看当前的站点域:

from django.conf import settings
from django.contrib.sites.models import Site

def my_view(request):
    current_site = Site.objects.get(id=settings.SITE_ID)
    if current_site.domain == 'foo.com':
        # Do something
    else:
        # Do something else.

Site 对象中获取 settings.SITE_ID 值的做法比较常见,因此 Site 模型管理器 (Site.objects ) 具备一个 get_current() 方法。 下面的例子与前一个是等效的:

from django.contrib.sites.models import Site

def my_view(request):
    current_site = Site.objects.get_current()
    if current_site.domain == 'foo.com':
        # Do something
    else:
        # Do something else.

注意

在这个最后的例子里,你不用导入 django.conf.settings

获取当前域用于呈现

正如情景二中所解释的那样,依据DRY原则(不做重复工作),你只需在一个位置储存站名和域名,然后引用当前 Site 对象的 namedomain 。例如: 例如:

from django.contrib.sites.models import Site
from django.core.mail import send_mail

def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...
    current_site = Site.objects.get_current()
    send_mail('Thanks for subscribing to %s alerts' % current_site.name,
        'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name,
        'editor@%s' % current_site.domain,
        [user_email])
    # ...

继续我们正在讨论的 LJWorld.com 和 Lawrence.com 例子,在Lawrence.com 该邮件的标题行是“感谢注册 Lawrence.com 提醒信件”。 在 LJWorld.com ,该邮件标题行是“感谢注册 LJWorld.com 提醒信件”。 这种站点关联行为方式对邮件信息主体也同样适用。

完成这项工作的一种更加灵活(但更重量级)的方法是使用 Django 的模板系统。 假定 Lawrence.com 和 LJWorld.com 各自拥有不同的模板目录( TEMPLATE_DIRS ),你可将工作轻松地转交给模板系统,如下所示:

from django.core.mail import send_mail
from django.template import loader, Context

def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...
    subject = loader.get_template('alerts/subject.txt').render(Context({}))
    message = loader.get_template('alerts/message.txt').render(Context({}))
    send_mail(subject, message, 'do-not-reply@example.com', [user_email])
    # ...

本例中,你不得不在 LJWorld.com 和 Lawrence.com 的模板目录中都创建一份 subject.txtmessage.txt 模板。 正如之前所说,该方法带来了更大的灵活性,但也带来了更多复杂性。

尽可能多的利用 Site 对象是减少不必要的复杂、冗余工作的好办法。

当前站点管理器

如果 站点 在你的应用中扮演很重要的角色,请考虑在你的模型中使用方便的 CurrentSiteManager 。 这是一个模型管理器(见第十章),它会自动过滤使其只包含与当前站点相关联的对象。

通过显示地将 CurrentSiteManager 加入模型中以使用它。 例如:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(max_length=100)
    pub_date = models.DateField()
    site = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager()

通过该模型, Photo.objects.all() 将返回数据库中所有的 Photo 对象,而 Photo.on_site.all() 仅根据 SITE_ID 设置返回与当前站点相关联的 Photo 对象。

换言之,以下两条语句是等效的:

Photo.objects.filter(site=settings.SITE_ID)
Photo.on_site.all()

CurrentSiteManager 是如何知道 Photo 的哪个字段是 Site 呢?缺省情况下,它会查找一个叫做 site 的字段。如果你的模型包含了名字不是site外键或者多对多关联,你需要把它作为参数传给CurrentSiteManager以显示指明。下面的模型拥有一个publish_on字段:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(max_length=100)
    pub_date = models.DateField()
    publish_on = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager('publish_on')

如果试图使用 CurrentSiteManager 并传入一个不存在的字段名, Django 将引发一个 ValueError 异常。

注意

即便是已经使用了 CurrentSiteManager ,你也许还想在模型中拥有一个正常的(非站点相关)的 管理器 。正如在附录 B 中所解释的,如果你手动定义了一个管理器,那么 Django 不会为你创建全自动的 objects = models.Manager() 管理器。

同样,Django 的特定部分(即 Django 超级管理站点和通用视图)使用在模型中定义 的第一个管理器,因此如果希望管理站点能够访问所有对象(而不是仅仅站点特有对象),请于定义 CurrentSiteManager 之前在模型中放入 objects = models.Manager()

Django如何使用多站点框架

尽管并不是必须的,我们还是强烈建议使用多站点框架,因为 Django 在几个地方利用了它。 即使只用 Django 来支持单个网站,你也应该花一点时间用 domainname 来创建站点对象,并将 SITE_ID 设置指向它的 ID 。

以下讲述的是 Django 如何使用多站点框架:

  • 在重定向框架中(见后面的重定向一节),每一个重定向对象都与一个特定站点关联。 当 Django 搜索重定向的时候,它会考虑当前的 SITE_ID

  • 在注册框架中,每个注释都与特定站点相关。 每个注释被显示时,其 site 被设置为当前的 SITE_ID ,而当通过适当的模板标签列出注释时,只有当前站点的注释将会显示。

  • 在 flatpages 框架中 (参见后面的 Flatpages 一节),每个 flatpage 都与特定的站点相关联。 创建 flatpage 时,你都将指定它的 site ,而 flatpage 中间件在获取 flatpage 以显示它的过程中,将查看当前的 SITE_ID

  • 在 syndication 框架中(参阅第 13 章), titledescription 的模板会自动访问变量 {{ site }} ,它其实是代表当前站点的 Site 对象。 而且,如果你不指定一个合格的domain的话,提供目录URL的钩子将会使用当前“Site”对象的domain。

  • 在权限框架中(参见十四章),视图django.contrib.auth.views.login把当前Site名字和对象分别以{{ site_name }}{{ site }}的形式传给了模板。

Flatpages(简单页面)

尽管通常情况下总是搭建运行数据库驱动的 Web 应用,有时你还是需要添加一两张一次性的静态页面,例如“关于”页面,或者“隐私策略”页面等等。 可以用像 Apache 这样的标准Web服务器来处理这些静态页面,但却会给应用带来一些额外的复杂性,因为你必须操心怎么配置 Apache,还要设置权限让整个团队可以修改编辑这些文件,而且你还不能使用 Django 模板系统来统一这些页面的风格。

这个问题的解决方案是使用位于 django.contrib.flatpages 开发包中的 Django 简单页面(flatpages)应用程序。该应用让你能够通过 Django 管理站点来管理这些一次性的页面,还可以让你使用 Django 模板系统指定它们使用哪个模板。 它在后台使用Django模型,这意味着它把页面项别的数据一样保存在数据库中,也就是说你可以使用标准Django数据库API来存取页面。

简单页面以它们的 URL 和站点为键值。 当创建简单页面时,你指定它与哪个URL以及和哪个站点相关联 。 (有关站点的更多信息,请查阅”多站点“一节。)

使用简单页面

安装简单页面应用程序必须按照下面的步骤:

  1. 添加 'django.contrib.flatpages'INSTALLED_APPS 设置。django.contrib.flatpages依赖django.contrib.sites,所以确保它们都在INSTALLED_APPS里。

  1. 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware' 添加到 MIDDLEWARE_CLASSES 设置中。

  1. 运行 manage.py syncdb 命令在数据库中创建必需的两个表。

简单页面应用程序在数据库中创建两个表: django_flatpagedjango_flatpage_sitesdjango_flatpage 只是将 URL 映射到标题和一段文本内容。 django_flatpage_sites 是一个多对多表,用于关联某个简单页面以及一个或多个站点。

该应用捆绑的 FlatPage 模型在 django/contrib/flatpages/models.py 进行定义,如下所示:

from django.db import models
from django.contrib.sites.models import Site

class FlatPage(models.Model):
    url = models.CharField(max_length=100, db_index=True)
    title = models.CharField(max_length=200)
    content = models.TextField(blank=True)
    enable_comments = models.BooleanField()
    template_name = models.CharField(max_length=70, blank=True)
    registration_required = models.BooleanField()
    sites = models.ManyToManyField(Site)

让我们逐项看看这些字段的含义:

  • url : 该简单页面所处的 URL,不包括域名,但是包含前导斜杠 (例如 /about/contact/ )。

  • title : 简单页面的标题。 框架不对它作任何特殊处理。 由你通过模板来显示它。

  • content : 简单页面的内容 (即 HTML 页面)。 框架不对它作任何特殊处理。 由你负责使用模板来显示。

  • enable_comments : 是否允许该简单页面使用评论。 框架不对它作任何特殊处理。 你可在模板中检查该值并根据需要显示评论窗体。

  • template_name : 用来解析该简单页面的模板名称。 这是一个可选项;如果未指定模板或该模板不存在,系统会退而使用默认模板 flatpages/default.html

  • registration_required : 是否注册用户才能查看此简单页面。 该设置项集成了 Djangos 验证/用户框架,该框架于第十四章详述。

  • sites : 该简单页面放置的站点。 该项设置集成了 Django 多站点框架,该框架在本章的“多站点”一节中有所阐述。

你可以通过 Django 超级管理界面或者 Django 数据库 API 来创建简单页面。 要了解更多内容,请查阅“添加、修改和删除简单页面”一节。

一旦简单页面创建完成, FlatpageFallbackMiddleware 将完成(剩下)所有的工作。 每当 Django 引发 404 错误,作为最后的办法,该中间件将根据所请求的 URL 检查简单页面数据库。 确切地说,它将使用所指定的 URL以及 SITE_ID 设置对应的站点 ID 查找一个简单页面。

如果找到一个匹配项,它将载入该简单页面的模板(如果没有指定的话,将使用默认模板 flatpages/default.html )。 同时,它把一个简单的上下文变量flatpage(一个简单页面对象)传递给模板。 模板解析过程中,它实际用的是RequestContext

如果 FlatpageFallbackMiddleware 没有找到匹配项,该请求继续如常处理。

注意

该中间件仅在发生 404 (页面未找到)错误时被激活,而不会在 500 (服务器错误)或其他错误响应时被激活。 还要注意的是必须考虑 MIDDLEWARE_CLASSES 的顺序问题。 通常,你可以把 FlatpageFallbackMiddleware 放在列表最后,因为它是最后的办法。

添加、修改和删除简单页面

可以用两种方式增加、变更或删除简单页面:

通过超级管理界面

如果已经激活了自动的 Django 超级管理界面,你将会在超级管理页面的首页看到有个 Flatpages 区域。 你可以像编辑系统中其它对象那样编辑简单页面。

通过 Python API

前面已经提到,简单页面表现为 django/contrib/flatpages/models.py 中的标准 Django 模型。这样,你就可以使用Django数据库API来存取简单页面对象,例如:

>>> from django.contrib.flatpages.models import FlatPage
>>> from django.contrib.sites.models import Site
>>> fp = FlatPage.objects.create(
...     url='/about/',
...     title='About',
...     content='<p>About this site...</p>',
...     enable_comments=False,
...     template_name='',
...     registration_required=False,
... )
>>> fp.sites.add(Site.objects.get(id=1))
>>> FlatPage.objects.get(url='/about/')
<FlatPage: /about/ -- About>

使用简单页面模板

缺省情况下,系统使用模板 flatpages/default.html 来解析简单页面,但你也可以通过设定 FlatPage 对象的 template_name 字段来更改特定简单页面的模板。

你必须自己创建 flatpages/default.html 模板。 只需要在模板目录创建一个 flatpages 目录,并把 default.html 文件置于其中。

简单页面模板只接受有一个上下文变量—— flatpage ,也就是该简单页面对象。

以下是一个 flatpages/default.html 模板范例:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
    "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<title>{{ flatpage.title }}</title>
</head>
<body>
{{ flatpage.content|safe }}
</body>
</html>

注意我们使用了safe模板过滤器来允许flatpage.content引入原始HTML而不必转义。

重定向

通过将重定向存储在数据库中并将其视为 Django 模型对象,Django 重定向框架让你能够轻松地管理它们。 比如说,你可以通过重定向框架告诉Django,把任何指向 /music/ 的请求重定向到 /sections/arts/music/ 。当你需要在站点中移动一些东西时,这项功能就派上用场了——网站开发者应该穷尽一切办法避免出现坏链接。

使用重定向框架

安装重定向应用程序必须遵循以下步骤:

  1. 'django.contrib.redirects' 添加到 INSTALLED_APPS 设置中。

  1. 'django.contrib.redirects.middleware.RedirectFallbackMiddleware' 添加到 MIDDLEWARE_CLASSES 设置中。

  1. 运行 manage.py syncdb 命令将所需的表添加到数据库中。

manage.py syncdb 在数据库中创建了一个 django_redirect 表。 这是一个简单的查询表,只有site_id、old_path和new_path三个字段。

你可以通过 Django 超级管理界面或者 Django 数据库 API 来创建重定向。 要了解更多信息,请参阅“增加、变更和删除重定向”一节。

一旦创建了重定向, RedirectFallbackMiddleware 类将完成所有的工作。 每当 Django 应用引发一个 404 错误,作为终极手段,该中间件将为所请求的 URL 在重定向数据库中进行查找。 确切地说,它将使用给定的 old_path 以及 SITE_ID 设置对应的站点 ID 查找重定向设置。 (查阅前面的“多站点”一节可了解关于 SITE_ID 和多站点框架的更多细节) 然后,它将执行以下两个步骤:

  • 如果找到了匹配项,并且 new_path 非空,它将重定向到 new_path

  • 如果找到了匹配项,但 new_path 为空,它将发送一个 410 (Gone) HTTP 头信息以及一个空(无内容)响应。

  • 如果未找到匹配项,该请求将如常处理。

该中间件仅为 404 错误激活,而不会为 500 错误或其他任何状态码的响应所激活。

注意必须考虑 MIDDLEWARE_CLASSES 的顺序。 通常,你可以将 RedirectFallbackMiddleware 放置在列表的最后,因为它是一种终极手段。

注意

如果同时使用重定向和简单页面回退中间件, 必须考虑先检查其中的哪一个(重定向或简单页面)。 我们建议将简单页面放在重定向之前(因此将简单页面中间件放置在重定向中间件之前),但你可能有不同想法。

增加、变更和删除重定向

你可以两种方式增加、变更和删除重定向:

通过管理界面

如果已经激活了全自动的 Django 超级管理界面,你应该能够在超级管理首页看到重定向区域。 可以像编辑系统中其它对象一样编辑重定向。

同过Python API

重定向表现为django/contrib/redirects/models.py 中的一个标准 Django 模型。因此,你可以通过Django数据库API来存取重定向对象,例如:

>>> from django.contrib.redirects.models import Redirect
>>> from django.contrib.sites.models import Site
>>> red = Redirect.objects.create(
...     site=Site.objects.get(id=1),
...     old_path='/music/',
...     new_path='/sections/arts/music/',
... )
>>> Redirect.objects.get(old_path='/music/')
<Redirect: /music/ ---> /sections/arts/music/>

CSRF 防护

django.contrib.csrf 开发包能够防止遭受跨站请求伪造攻击 (CSRF).

CSRF, 又叫会话跳转,是一种网站安全攻击技术。 当某个恶意网站在用户未察觉的情况下将其从一个已经通过身份验证的站点诱骗至一个新的 URL 时,这种攻击就发生了,因此它可以利用用户已经通过身份验证的状态。 乍一看,要理解这种攻击技术比较困难,因此我们在本节将使用两个例子来说明。

一个简单的 CSRF 例子

假定你已经登录到 example.com 的网页邮件账号。该网站有一个指向example.com/logout的注销按钮。就是说,注销其实就是访问example.com/logout

通过在(恶意)网页上用隐藏一个指向 URL example.com/logout<iframe> ,恶意网站可以强迫你访问该 URL 。因此,如果你登录 example.com 的网页邮件账号之后,访问了带有指向 example.com/logout<iframe> 的恶意站点,访问该恶意页面的动作将使你登出 example.com 。 Thus, if you’re logged in to the example.com webmail account and visit the malicious page that has an <iframe> to example.com/logout , the act of visiting the malicious page will log you out from example.com .

很明显,登出一个邮件网站也不是什么严重的安全问题。但是同样的攻击可能针对任何相信用户的站点,比如在线银行和电子商务网站。这样的话可能在用户不知情的情况下就下订单付款了。

稍微复杂一点的CSRF例子

在上一个例子中, example.com 应该负部分责任,因为它允许通过 HTTP GET 方法进行状态变更(即登入和登出)。 如果对服务器的状态变更要求使用 HTTP POST 方法,情况就好得多了。 但是,即便是强制要求使用 POST 方法进行状态变更操作也易受到 CSRF 攻击。

假设 example.com 对登出功能进行了升级,登出 <form> 按钮是通过一个指向 URL example.com/logoutPOST 动作完成,同时在 <form> 中加入了以下隐藏的字段:

<input type="hidden" name="confirm" value="true">

这就确保了用简单的指向example.com/logoutPOST 不会让用户登出;要让用户登出,用户必须通过 POSTexample.com/logout 发送请求 并且发送一个值为’true’的POST变量。 confirm

尽管增加了额外的安全机制,这种设计仍然会遭到 CSRF 的攻击——恶意页面仅需一点点改进而已。 攻击者可以针对你的站点设计整个表单,并将其藏身于一个不可见的 <iframe> 中,然后使用 Javascript 自动提交该表单。

防止 CSRF

那么,是否可以让站点免受这种攻击呢? 第一步,首先确保所有 GET 方法没有副作用。 这样以来,如果某个恶意站点将你的页面包含为 <iframe> ,它将不会产生负面效果。

该技术没有考虑 POST 请求。 第二步就是给所有 POST 的form标签一个隐藏字段,它的值是保密的并根据用户进程的 ID 生成。 这样,从服务器端访问表单时,可以检查该保密的字段。不吻合时可以引发一个错误。

这正是 Django CSRF 防护层完成的工作,正如下面的小节所介绍的。

使用CSRF中间件

django.contrib.csrf 开发包只有一个模块: middleware.py 。该模块包含了一个 Django 中间件类—— CsrfMiddleware ,该类实现了 CSRF 防护功能。

在设置文件中将 'django.contrib.csrf.middleware.CsrfMiddleware' 添加到 MIDDLEWARE_CLASSES 设置中可激活 CSRF 防护。 该中间件必须在 SessionMiddleware 之后 执行,因此在列表中 CsrfMiddleware 必须出现在 SessionMiddleware 之前 (因为响应中间件是自后向前执行的)。 同时,它也必须在响应被压缩或解压之前对响应结果进行处理,因此 CsrfMiddleware 必须在 GZipMiddleware 之后执行。一旦将它添加到MIDDLEWARE_CLASSES设置中,你就完成了工作。 参见第十五章的“MIDDLEWARE_CLASSES顺序”小节以了解更多。

如果感兴趣的话,下面是 CsrfMiddleware 的工作模式。 它完成以下两项工作:

  1. 它修改当前处理的请求,向所有的 POST 表单增添一个隐藏的表单字段,使用名称是 csrfmiddlewaretoken ,值为当前会话 ID 加上一个密钥的散列值。 如果未设置会话 ID ,该中间件将 不会 修改响应结果,因此对于未使用会话的请求来说性能损失是可以忽略的。

  1. 对于所有含会话 cookie 集合的传入 POST 请求,它将检查是否存在 csrfmiddlewaretoken 及其是否正确。 如果不是的话,用户将会收到一个 403 HTTP 错误。 403 错误页面的内容是检测到了跨域请求伪装。 终止请求。

该步骤确保只有源自你的站点的表单才能将数据 POST 回来。

该中间件特意只针对 HTTP POST 请求(以及对应的 POST 表单)。 如我们所解释的,永远不应该因为使用了 GET 请求而产生负面效应,你必须自己来确保这一点。

未使用会话 cookie 的 POST 请求无法受到保护,但它们也不 需要 受到保护,因为恶意网站可用任意方法来制造这种请求。

为了避免转换非 HTML 请求,中间件在编辑响应结果之前对它的 Content-Type 头标进行检查。 只有标记为 text/htmlapplication/xml+xhtml 的页面才会被修改。

CSRF中间件的局限性

CsrfMiddleware 的运行需要 Django 的会话框架。 (参阅第 14 章了解更多关于会话的内容。)如果你使用了自定义会话或者身份验证框架手动管理会话 cookies,该中间件将帮不上你的忙。

如果你的应用程序以某种非常规的方法创建 HTML 页面(例如:在 Javascript 的document.write语句中发送 HTML 片段),你可能会绕开了向表单添加隐藏字段的过滤器。 在此情况下,表单提交永远无法成功。 (这是因为在页面发送到客户端之前,CsrfMiddleware使用正则表达式来添加csrfmiddlewaretoken字段到你的HTML中,而正则表达式不能处理不规范的HTML。)如果你怀疑出现了这样的问题。使用你浏览器的查看源代码功能以确定csrfmiddlewaretoken是否插入到了表单中。

想了解更多关于 CSRF 的信息和例子的话,可以访问 http://en.wikipedia.org/wiki/CSRF

人性化数据

django.contrib.humanize包含了一些是数据更人性化的模板过滤器。 要激活这些过滤器,请把'django.contrib.humanize'加入到你的INSTALLED_APPS中。完成之后,向模版了加入{% load humanize %}就可以使用下面的过滤器了。

apnumber

对于 1 到 9 的数字,该过滤器返回了数字的拼写形式。 否则,它将返回数字。 这遵循的是美联社风格。

举例:

  • 1 变成 one 。

  • 2 变成 two 。

  • 10 变成 10 。

你可以传入一个整数或者表示整数的字符串。

intcomma

该过滤器将整数转换为每三个数字用一个逗号分隔的字符串。

例子:

  • 4500 变成 4,500 。

  • 45000 变成 45,000 。

  • 450000 变成 450,000 。

  • 4500000 变成 4,500,000 。

可以传入整数或者表示整数的字符串。

intword

该过滤器将一个很大的整数转换成友好的文本表示方式。 它对于超过一百万的数字最好用。

例子:

  • 1000000 变成 1.0 million 。

  • 1200000 变成 1.2 million 。

  • 1200000000 变成 1.2 billion 。

最大支持不超过一千的五次方(1,000,000,000,000,000)。

可以传入整数或者表示整数的字符串。

ordinal

该过滤器将整数转换为序数词的字符串形式。

例子:

  • 1 变成 1st 。

  • 2 变成 2nd 。

  • 3 变成 3rd 。

  • 254变成254th。

可以传入整数或者表示整数的字符串。

标记过滤器

django.contrib.markup包含了一些列Django模板过滤器,每一个都实现了一中通用的标记语言。

每种情形下,过滤器都期望字符串形式的格式化标记,并返回表示标记文本的字符串。 例如:textile过滤器吧Textile格式的文本转换为HTML。

{% load markup %}
{{ object.content|textile }}

要激活这些过滤器,仅需将 'django.contrib.markup' 添加到 INSTALLED_APPS 设置中。 一旦完成了该项工作,在模板中通过 {% load markup %} 就能使用这些过滤器。 要想掌握更多信息的话,可阅读 django/contrib/markup/templatetags/markup.py. 内的源代码。

下一章

这些继承框架(CSRF、身份验证系统等等)通过提供 中间件 来实现其奇妙的功能。中间件是在请求之前/后执行的可以修改请求和响应的代码,它扩展了框架。 在下一章,我们将介绍Django的中间件并解释怎样写出自己的中间件。

GNU Free Document License约束。 谨奉