The Django Book

第十四章 集成的子框架

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

Django标准库

Django的标准库存放在 django.contrib 包中。每个子包都是一个独立的附加功能包。 它们互相之间一般没有必然的关联,但是有些 django.contrib 子包可能依赖其他的包。

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

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

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

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

  • auth : Django的用户验证框架。请查看第12章

  • comments : 一个评论应用,目前,这个应用正在紧张的开发中,因此在本书出版的时候还不能给出一个完整的说明,关于这个应用的更多信息请参见Django的官方网站.

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

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

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

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

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

  • redirects : 用来管理重定向的框架。参见后面标题为《重定向》的小节。

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

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

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

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

本章接下来将详细描述前面没有介绍过的 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 表安装到数据库中。

  1. 通过 Django 管理后台或通过 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(maxlength=200)
    # ...
    sites = models.ManyToManyField(Site)

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

from django.conf import settings

def article_detail(request, article_id):
    try:
        a = Article.objects.get(id=article_id, sites__id=settings.SITE_ID)
    except Article.DoesNotExist:
        raise Http404
    # ...

该视图方法是可重用的,因为它根据 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(maxlength=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 (Dont Repeat Yourself) 方法(在一个位置储存站名和域名)来说,只需引用当前 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 对象是减少不必要的复杂、冗余工作的好办法。

获取当前域的完整 URL

Django 的 get_absolute_url() 约定对与获取不带域名的对象 URL 非常理想,但在某些情形下,你可能想显示某个对象带有 http:// 和域名以及所有部分的完整 URL 。要完成此工作,你可以使用多站点框架。下面是个简单的例子:

>>> from django.contrib.sites.models import Site
>>> obj = MyModel.objects.get(id=3)
>>> obj.get_absolute_url()
'/mymodel/objects/3/'
>>> Site.objects.get_current().domain
'example.com'
>>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'http://example.com/mymodel/objects/3/'

当前站点管理器

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

通过显示地将 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(maxlength=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(maxlength=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 框架中(参阅第 11 章), titledescription 的模板自动访问变量 {{ site }} ,它就是代表当前着桨的 Site 对象。而且,如果你不指出一个完全合格的domain的话,提供目录URLS的钩子将会使用当前“Site”对象的domain。

  • 在身份验证框架(参见第 12 章)中, django.contrib.auth.views.login 视图将当前 Site 名称作为 {{ site_name }} 传递给模板。

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(maxlength=100)
    title = models.CharField(maxlength=200)
    content = models.TextField()
    enable_comments = models.BooleanField()
    template_name = models.CharField(maxlength=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(
...     url='/about/',
...     title='About',
...     content='<p>About this site...</p>',
...     enable_comments=False,
...     template_name='',
...     registration_required=False,
... )
>>> fp.save()
>>> 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 }}
</body>
</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_idold_pathnew_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(
...     site=Site.objects.get(id=1),
...     old_path='/music/',
...     new_path='/sections/arts/music/',
... )
>>> red.save()
>>> Redirect.objects.get(old_path='/music/')
<Redirect: /music/ ---> /sections/arts/music/>

CSRF 防护

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

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

一个简单的 CSRF 例子

假定你已经登录到 example.com 的网页邮件账号。该网页邮件站点上有一个登出按钮指向了 URL example.com/logout ,换句话说,要登出的话,需要做的唯一动作就是访问 URL : example.com/logout

通过在(恶意)网页上用隐藏一个指向 URL example.com/logout<iframe> ,恶意网站可以强迫你访问该 URL 。因此,如果你登录 example.com 的网页邮件账号之后,访问了带有指向 example.com/logout<iframe> 的恶意站点,访问该恶意页面的动作将使你登出 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" />

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

尽管增加了额外的安全机制,这种设计仍然会遭到 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 设置中,你就完成了工作。参阅第 13 章中的《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 的会话框架。(参阅第 12 章了解更多关于会话的内容)如果你使用了自定义会话或者身份验证框架手动管理会话 cookies,该中间件将帮不上你的忙。

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

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

人性化数据

该应用程序包括一系列 Django 模板过滤器,用于增加数据的人性化。要激活这些过滤器,仅需将 '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 。

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

标记过滤器

下列模板过滤器集合实现了常见的标记语言:

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

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

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

下一步?

这些继承框架(CSRF、身份验证系统等等)通过提供 中间件 来实现其奇妙的功能。本质上,中间件就是在每个请求之前或/和之后运行的代码,它们可随意修改每个请求或响应。接下来,我们将讨论 Django 的内建中间件,并解释如何编写自己的中间件。

Copyright 2006 Adrian Holovaty and Jacob Kaplan-Moss.
This work is licensed under the GNU Free Document License.
Hosting graciously provided by media temple
Chinese translate hosting by py3k.cn.