Alan Hou的个人博客

Django 3网页开发指南第4版 第7章 安全和性能

完整目录请见:Django 3网页开发指南 – 第4版

本章中包含如下小节:

引言

如果软件不当地暴露敏感信息,让用户陷入漫长的等待或是消耗大量的硬件资源,那么就很难持久存在。开发者有责任保证应用的安全和高性能。本章中,我们将查看一些方式来让用户(以及你自己)在Django应用内进行操作时保持安全。然后,我们会讲解一些降低处理时间的缓存选择,并让获取用户数据在金钱和时间层面上都实现低开销。

技术要求

运行本章的代码要求安装最新稳定版的Python 3、MySQL或PostgreSQL数据库以及通过虚拟环境创建的Django项目。

可在GitHub仓库的Chapter07目录中查看本章的代码。

让表单免受跨站请求伪造(CSRF)攻击

不加以适当的预防措施,恶意站点可以对你的网站进行某些请求,进而对你的服务器进行一些预料外的修改。例如,可以影响到用户认证或在未经用户许可的情况下修改内容。Django自带有一套防止这类CSRF攻击的系统,本小节中我们就来进行了解。

准备工作

使用第3章 表单和视图使用CRUDL函数创建应用一节所创建的ideas应用。

如何实现…

按照如下步骤来启用Django中的CSRF防护:

  1. 确保在配置文件中包含了CsrfViewMiddleware,如下所示:
  2. 确保表单视图使用request上下文进行得渲染。例如,在已有的ideas应用中有如下内容:
  3. 在用于表单的模板中,应用使用POST方法并包含{% csrf_token %}标签:
  4. 如果在表单布局中使用了django-crispy-forms,默认会包含CSRF令牌:

实现原理…

Django使用隐藏字段来防止CSRF攻击。由服务端根据具体请求和随机信息生成一个令牌。再借由CsrfViewMiddleware自动让该令牌在请求上下文中可用。虽然不建议禁用这一中间件,但通过应用@csrf_protect装饰器也可以标记单独的视图来实现这一行为:

类似地,即使启用了这一中间件,我们也可以通过使用@csrf_exempt装饰器对单独视图排除CSRF检查:

内置的 {% csrf_token %} 标签生成一个带有令牌的隐藏input字段,如下例所示:

在使用GET、HEAD、OPTIONS或TRACE方法提交请求的表单中包含令牌视作无效,因此使用这些方法的请求首先不应产生副面效应。大多数情况下,要求进行CSRF保护的web表单使用POST请求。

受保护的表单使用不带所要求token进行提交时,Django的内置表单校验会识别出并彻底拒绝请求。仅在提交中包含具有有效值的token时才允许进行一步处理。其结果就是外部网站无法对你的服务端做出修改,因为它们无法知晓并包含当前有效的token值。

扩展知识…

在很多情况下,改善表单让其可通过Ajax进行提交会比较好。这时还需要使用CSRF令牌进行保护,可以在每个请求中以附加数据注入这个信息,使用这一方法要求开发者记住在每个POST请求中这么操作。另一种方法是使用 已有的CSRF令牌header,这样更为高效。

首先,令牌值需要主动获取,如何获取由CSRF_USE_SESSIONS配置值所决定。在值为True时,信息存储在session中而非cookie中,因此我们必须使用 {% csrf_token %}标签并在DOM中包含它。然后我们可以在JavaScript中读取该元素来获取这一数据:

CSRF_USE_SESSIONS配置默认是False状态,令牌值的获取数据源是csrftoken的cookie。虽然可以自己实现cookie操作方法,但有很多可用的工具可以简化这一过程。例如,我们可以使用js-cookie API来通过名称更新轻易地提取令牌,如下所示:

一旦提取出令牌,需要为XmlHttpRequest将其设置为CSRF令牌头部。虽然这可以通过每次请求分别来完成,但这样会和对每个请求添加请求参数数据存在同样的缺点。我们可以使用jQuery及其功能来在发送前自动对每个请求添加数据,如下:

相关内容

使用内容安全策略(CSP)让请求安全

动态多用户网站通常允许用户添加多种类型的媒体文件:图片、视频、音频、HTML、JavaScript代码段等等。这会让用户有可能通过对网站添加恶意代码来窃取cookie或个人信息、在后台调用计划外的Ajax请求或对其它用户产生不利。现代浏览器支持额外的安全层,对你的媒体资源来源设置白名单。这称之为CSP,在本节中,我们将展示如何在Django站点中使用它。

准备工作

我们使用已有的Django项目,比如第3章 表单和视图中包含有ideas应用的代码。

如何实现…

通过如下步骤来实现CSP的保护:

  1. 在你的虚拟环境中安装django-csp:
  2. 在配置文件中添加CSPMiddleware:
  3. 在同一配置文件中,添加django-csp配置用于对于你所信任的媒体来源设置白名单,例如jQuery和Bootstrap的CDN(在实现原理…一节中会进行详细讲解):
  4. 如果在模板中存在行内脚本或样式,使用加密的nonce对设置白名单,如下所示:

实现原理…

可以在head版块或响应头中添加CSP至meta标签中:

CSP允许我们定义资源类型并允许以彼此为数据源。可以使用的主要指令如下:

ℹ️ 完整的资源类型列表及对应的配置项分别参见CSP类型Django CSP配置

每条指令的值可为以下列表中的一个或多个(引号应保留):

没有什么绝对安全的通用django-csp配置方式。这是一个试错的过程。但以下我们提供一些纲领:

  1. 先对已运行项目添加CSP。过度的限制只会让开发网站变得过于困难。
  2. 查看硬编码至模板中的脚本、样式、字体和其它静态文件并设置白名单。
  3. 如果允许将媒体文件嵌入到博客文章或其它动态内容中的话允许所有的图片、媒体文件及frame的数据源,如下:
  4. 如果使用行内脚本或样式的话,对它们添加nonce=”{{ request.csp_nonce }}”。
  5. 避免使用CSP值’unsafe-inline’ 和 ‘unsafe-eval’,除非只能选择在模板中通过硬编码将HTML添加到网站中。
  6. 浏览整个网站并查看有哪些没有正确加载的内容。如果在开发者控制台中看到如下消息,则表示内容受到了CSP的限制:拒绝执行行内脚本,因为即违反了下述的内容安全策略指令:”script-src ‘self’ https://stackpath.bootstrapcdn.com/ https://code.jquery.com/ https://cdnjs.cloudflare.com/”。需要使用’unsafe-inline’关键词、哈希 (‘sha256- P1v4zceJ/oPr/yp20lBqDnqynDQhHf76lljlXUxt7NI=’)或nonce(‘nonce-…’) 来进行行内执行。

    这类错误通过发生在第三方工具(如django-cms、Django调试工具栏或谷歌分析)尝试通过JavaScript包含某一资源时。可以通过像在上面错误消息中所看到的数据源哈希’sha256-P1v4zceJ/oPr/yp20lBqDnqynDQhHf76lljlXUxt7NI=’.那样来对这些资源添加白名单。
  7. 如果你在开发一个现代增强型网页应用(PWA),应检查下由CSP_MANIFEST_SRC和CSP_WORKER_SRC配置所控制的声明指令及网页worker。

相关内容

使用django-admin-honeypot

如果保留Django站点的默认后台访问路径,会存在让黑客进行暴力攻击以及使用他们列表中的不同密码进行登录尝试的风险。有一个名为django-admin-honeypot的应用,允许我们伪装登录页面并监测这些暴力攻击行为。本节中就学习如何来使用。

准备工作

可以使用任意想要进行安全保护的Django项目。例如,我们可以对前一小节中的项目进行扩展。

如何实现…

按照如下步骤来配置django-admin-honeypot:

  1. 在虚拟环境中安装该模块:
  2. 将admin_honeypot添加到配置文件的INSTALLED_APPS中:
  3. 修改URL规则:

实现原理…

此时如果访问默认的后台URLhttp://127.0.0.1:8000/en/admin/,可以看到如下登录页面,但不论输入什么内容都会返回无效密码:

TODO

现在真实的后台地址为ttp://127.0.0.1:8000/en/management/,其中可以通过蜜罐查看到所追踪的登录信息。

扩展知识…

在编写本书时,django-admin-honeypot 在Django 3.0下还无法完好运行,后台界面对HTML进行了转义,而其实应该进行安全渲染。在django-admin-honeypot发布更新版本之前,我们可以通过一些小修改来进行解决,如下:

  1. 通过admin.py文件创建名为admin_honeypot_fix的应用,代码如下:
  2. 同样在该应用中,使用如下新的应用配置创建apps.py文件:
  3. 在本文中的INSTALLED_APPS替换admin_honeypot为新的应用配置:

蜜罐捕获的登录尝试如下所示:

TODO

相关内容

实现密码校验

软件安全问题中排在前列的要属用户选择了不安全的密码。本节中,我们将学习如何通过内置及自定义的密码校验器来强制实现最低密码的要求,这样用户会被引导设置更为安全的密码。

准备工作

打开项目的配置文件定位到AUTH_PASSWORD_VALIDATORS配置项。同时新建一个auth_extra应用并添加password_validation.py 文件。

如何实现…

按照如下步骤来为项目设置等级更强的密码:

  1. 自定义校验器的Django配置,添加如下选项:
  2. 在新的auth_extra应用的password_validation.py文件中添加MaximumLengthValidator类,如下所示:
  3. 同样在该文件中创建SpecialCharacterInclusionValidator类:
  4. 然后在配置中新增校验器:

实现原理…

Django中包含很多默认的密码校验器:

在使用startproject管理命令新建项目时,会使用默认选项添加它们为初始校验器。本节中,我们将展示如何按我们的项目需要调整这些选项,将密码最小长度提升为12个字符。

对于UserAttributeSimilarityValidator,我们将还将max_similarity减到了0.5,这表示密码要比默认与这些用户属性具有更大的差别。

在password_validation.py中,我们定义了两个新的校验器:

每个校验器类必须要有两个方法:

最后,我们在配置中添加了一个新验证器,用于重写默认项为密码最大长度为32个字符,并在默认特殊字符列表中增加符号{、}、^和&。

扩展知识…

AUTH_PASSWORD_VALIDATORS中所提供的验证器在执行createsuperuser和changepassword管理命令以及用内置表单修改或重置密码时会自动执行。但有时会希望对自定义的密码管理命令执行同样的验证。Django为这一级别的集成提供了一些函数,可在django.contrib.auth.password_validation模块中社区贡献的Django auth应用中查看详情。

相关内容

下载授权文件

有时可能希望只有指定人员下载网站上具有知识产权的内容。例如 ,音乐、视频、文学或其它艺术作品只允许付费会员访问。本节中,我们将学习如何使用社区的Django auth应用来仅允许授权的用户进行图片下载。

准备工作

我们使用第3章 表单和视图中所创建的ideas应用。

如何实现…

逐一执行如下步骤:

  1. 创建一个视图要求认证后才能下载文件,如下所示:
  2. 在URL配置文件中添加下载的视图:
  3. 在项目的URL配置文件中设置登录视图:
  4. 为登录表单创建一个模板,如以下代码所示:
  5. 在idea详情页模板中,添加一个下载链接:

应当限制用户绕过Django直接下载受限文件。如果运行 Apache 2.4,在Apache web 服务器上可以在media/ideas目录的.htaccess文件中添加如下内容:

ℹ️在使用django-imagekit时,本书中一直有使用,所生成的图片存储于media/CACHE目录中对外提供服务,因为我们的.htaccess配置不会对其产生影响。

实现原理…

download_idea_picture视图将数据流指向具体idea的原始上传图片。设置为attachment的Content-Disposition头使得图片可以进行下载,不在浏览器中直接展示 。该头部还设置了文件名,名称类似于gamified-donation-platform.jpg。如果该idea不存在这张力片,会返回一个404页面,显示一个简单的消息:Picture unavailable。

@login_required装饰器会在用户未登录而尝试访问下载文件时将访客重定向至登录页面。默认登录页面如下:

TODO

相关内容

对图片添加动态水印

有时,可以让用户看到图片,但因知识产权和艺术版权等原因不允许重新发布。本节中,我们将学习如何对显示在网站上的图片应用水印。

准备工作

我们使用第3章 表单和视图中的使用CRUDL函数创建应用一节中所创建的core和ideas应用。

如何实现…

按照如下步骤对所显示的idea图片应用水印:

  1. 如未安装,请先在你的虚拟环境中安装django-imagekit:
  2. 将”imagekit”添加至配置文件的INSTALLED_APPS中:
  3. 在core应用中,创建名为processors.py的文件并在其中添加WatermarkOverlay类,如下:
  4. 在Idea模型中的picture下面添加watermarked_picture_large规格,如下所示:
  5. 使用你自己喜欢的图片处理软件,创建一个半透明的PNG图片,在透明背景上包含白色文本或logo。将尺寸设置为800 x 400 px。将图片保存至site_static/site/img/watermark.png。效果类似下面这样:
    TODO
  6. 然后运行collectstatic管理命令:
  7. 编辑idea详情模板并在其中添加打水印后的图片,如下:

实现原理…

如果访问idea详情页,会看到遮罩了水印的大图,类似下面这样:

TODO

我们来了解下如何实现的。在详情模板中,<img>标签的src属性使用idea的图片规格,如watermarked_picture_large,位于media/CACHE/目录中创建了修改后的图片,从该处提供数据。

django-imagekit规格使用处理器(processor)来改变图片。这里使用了两个处理器:

django-imagekit处理器有一个从前置处理器获取图片的process()方法,返回新修改后的图片。本例中,我们处理原始图片及半透明遮罩获取结果。

相关内容

使用Auth0进行认证

人们交互的服务数量与日俱增,他们需要记住的用户名和密码也同样在增加。除此之外,每个存储用户信息的地方从安全泄漏的角度都会增加一处被窃取的位置。为削弱这种问题,Auth0这样的服务把认证服务集中到单个安全的平台上。

除支持用户名和密码信息史上,Auth0还具体通过社交平台如Google、Facebook或Twitter进行认证的功能。可以使用通过短信或邮件发送的单次验证码实现无密码登录,甚至还有对于不同服务的企业级支持。本节中,我们将学习如何对接Auth0应用至Django中,以及如何进行集成来处理用户认证。

准备工作

尚未创建Auth0应用的用户可在https:/​/​auth0.​com/​​进行创建,按照下面的提示进行配置。在免费方案中提供了两个社交账户连接,因此我们激活Google和Twitter来实现登录。用户也可以尝试其它服务。注意有些账户要求注册一个应用并获取API key及密钥。

接下来,我们需要在项目中安装python-social-auth及一些其它依赖。在pip安装文件中添加如下依赖:

ℹ️social-auth-app-django 是python- social-auth项目特别针对Django的包,它让我们可以使用某一社交账户来进行网站用户认证。

使用pip在虚拟环境中安装这些依赖。

如何实现…

按照如下步骤将Auth0连接到你的Django项目中:

  1. 在配置文件中对INSTALLED_APPS添加社交认证应用,如下:
  2. 接着添加social_django应用所需的Auth0配置,类似下面这样:

    确保在密钥或环境变量中定义AUTH0_DOMAIN、AUTH0_KEY和AUTH0_SECRET。这些变量的值可以在本小节准备工作的第1步中所创建的Auth0应用配置中查找到。
  3. 我们需要为Auth0连接创建一个后台,如下例所示:
  4. 在AUTHENTICATION_BACKENDS配置中添加这个新后台,如以下代码所示:
  5. 我们希望可以在任意模板中访问这些社交认证用户。因此将为其创建一个上下文处理器:
  6. 然后我们在配置文件中进行注册:
  7. 下面创建首页、仪表盘和登出页的视图:
  8. 创建首页模板如下:
  9. 相应地创建一个仪表盘模板:
  10. 更新URL规则:
  11. 最后添加登录URL配置:

实现原理…

如果在浏览器中访问项目的首页,会看到邀请登录的链接 。点击后会被重定向到Auth0的认证系统,界面像下面这样:

TODO

这是python-social-auth内部实现的,通过相关联的SOCIAL_AUTH_*设置配置一个Auth0后台。

成功完成登录后,Auth0后台接收到来自响应的数据并进行处理。相关联的数据添加到请求相关的用户对象中。在仪表盘视图中,即通过处理LOGIN_REDIRECT_URL到达的认证结果 页,会提取用户详情并添加到模板上下文中。之后渲染dashboard.html。结果如下所示:

TODO

仪表盘中展示的登出按钮,在点击后会进行登出用户的处理。

相关内容

缓存方法返回值

在请求-响应环中调用需重度计算或多次进行数据库查询的方法时,视图的性能可能会变得很低。本节中,我们将学习可用于缓存方法返回值以供稍后反复使用的模式。注意这里没有使用Django的缓存框架,它是Python默认提供的。

准备工作

选择一个带有包含有耗时方法并在请求-响应环中反复使用的模型的应用。

如何实现…

执行如下步骤:

  1. 这种模式可用于缓存模型的方法返回值,供视图、表单或模板重复使用,如下所示:
  2. 例如,我们为ViralVideo模型创建一个get_thumbnail_url()方法。在第10章 锦上添花使用数据库查询表达式一节里我们会学习到更多的详情:

实现原理…

在这个通用的示例中,方法查看有没有模型的_expensive_value_cached 属性存在。如果不存在,执行耗时的运算并将结果赋值给新属性。在方法的最后,返回缓存值。当然,如果有多个比较重的方法,会需要使用不同的属性名称来保存每个运算所得的值。

此时可以在模板的头部和底部使用{{ object.some_expensive_function }}这样的表单,耗时运算只会执行一次。

在模板中,还可以在{% if %}条件表达式及值的输出中使用该函数,如下所示:

在另一个示例中,会们通过解析视频嵌入代码获取其ID并拼装出缩略图的URL,来查看YouTube视频的缩略图。这样就可以使用如下的模板:

扩展知识…

我们刚刚描述的方法仅在方法无需传入参数、每次结果一致时有作用。但如果输入值发生变化会怎样呢?从Python 3.2开始,我们用于提供方法调用最近最少使用(LRU)缓存的装饰器基于参数的哈希(至少对于那些可哈希的参数如此)。

例如,我们来看一个虚构的小示例,其中函数接收两个参数并返回一些复杂逻辑运算后的结果:

如果我们有这样一个函数,希望提供用于存储一些常用输入变化结果的缓存,可以轻松地使用functools包中的@lru_cache装饰器,如下所示:

这样我们提供了一个通过输入值哈希的键进行存储多达100个结果的缓存机制。typed选项于Python 3.3开始加入,通过将其指定为True,我们可以将a=1和b=2 与 a=1.0和b=2.0分别进行单独存储。根据逻辑运算和返回值的不同,这种变化可能适用也可能不适用。

ℹ️可以通过functools文档了解更多有关@lru_cache装饰器的知识。

我们还可以对本节前面的示例使用这一装饰器简化代码,如下所示:

相关内容

使用Memcached缓存Django视图

Django让我们可以通过缓存开销比大的部分来提升请求-响应环的速度,比如数据库查询或模板渲染。Django原生支持的最快速、最可靠的缓存是基于内存的缓存服务端Memcached。本节中,我们将学习如何使用Memcached来缓存viral_videos应用中的视图。我们会在第10章 锦上添花使用数据库查询表达式一节中做更进一步的了解。

准备工作

我们需要做很多事来为Django项目准备缓存:

  1. 先安装memcached服务。例如macOS上最简单的方式是使用Homebrew:
  2. 然后可以使用如下命令启动、停止或重启Memcached服务:

    ℹ️在其它操作系统中,可以使用apt-get、yum等默认包管理工具安装Memcached。还有一种方式是通过源码进行编译安装,参见https://memcached.org/downloads。

  3. 在虚拟环境中安装Memcached Python包:

如何实现…

对指定的视图集成缓存,执行如下步骤:

  1. 在项目配置文件中进行CACHES的设置,如下:
  2. 确保在secrets文件或环境变量中将CACHE_LOCATION设置为localhost:11211。
  3. 修改viral_videos应用的视图如下:

ℹ️如果按照下一节中的Redis配置进行操作,会发现在views.py文件中无需进行什么修改。这表示我们可以不对代码进行修改即可修改底层的缓存机制并投入使用。

实现原理…

稍后我们学习第10章 锦上添花使用数据库查询表达式一节时会发现,爆款视频详情视图通过登录和匿名用户展示访问数。如果访问了某个爆款视频(如http://127.0.0.1:8000/en/videos/1/)并在启用了缓存的情况下多次刷新页面,会发现访问数每分钟仅产生一次变化 。这是因为对于每个用户响应内容会缓存60秒。我们通过使用@cache_page装饰器来对视图设置缓存。

Memcached是一种键值对存储,它默认使用完整URL来生成每一缓存页的键。在两个访客同时访问同一页面时,第一个访客会接收到Python代码所生成的页面,而第二个用户则会从Memcached服务中获取到相同的HTML代码。

在我们的示例中,要让每个访客即使访问同一URL时进行分别对待的话,可以使用@vary_on_cookie装饰器。这一装饰器查看HTTP请求中Cookie头部的唯一性。

💡可以通过官方文档学习更多有关Django缓存框架的知识。同样也可以通过https://memcached.org学习更多有关Memcached的知识。

相关内容

使用Redis缓存Django视图

虽然作为缓存机制Memcached在市场上有着稳固的地位并且Django也可以很好的支持。Redis是一套提供有Memcached所有功能并具有更多功能的替代系统。这里我们将回顾使用Memcached缓存Django视图一节中的流程,学习如何使用Redis来实现同样的功能。

准备工作

对Django项目添加缓存需要完成一些操作:

  1. 安装Redis服务。例如,在macOS中最简单的安装方式是使用Homebrew:
  2. 然后我们可以使用如下命令来启动、停止或重启Redis服务:

    ℹ️在其它操作系统下,可以使用apt-get、yum或其它的包管理工具来安装Redis。另一种方式是通过源码进行编译安装,参见https://redis.io/download。

  3. 在虚拟环境中对Django安装Redis缓存后台及其依赖,如下:

如何实现…

执行如下步骤来对指定的视图集成缓存:

  1. 在项目配置文件中设置CACHES,如下:
  2. 确保在secrets文件或环境变量中将CACHE_LOCATION设置为localhost:6379。
  3. 修改viral_videos应用的视图如下:

ℹ️如果按照前一节中的Memcached配置进行操作,会发现在views.py文件中无需进行什么修改。这表示我们可以不对代码进行修改即可修改底层的缓存机制并投入使用。

实现原理…

和Memcached一样,我们使用@cache_page装饰器对视图设置缓存。因此每条响应对每个用户都有60秒的缓存。爆款视频详情视图通过登录和匿名用户展示访问数。如果访问了某个爆款视频(如http://127.0.0.1:8000/en/videos/1/)并在启用了缓存的情况下多次刷新页面,会发现访问数每分钟仅产生一次变化 。

和Memcached相同,Redis是一种键值对存储,它根据完整URL生成每一缓存页的键。在两个访客同时访问同一页面时,第一个访客会接收到Python代码所生成的页面,而第二个访客则会从Redis服务端获取到相同的HTML代码。

在本例中,要让每个访客在访问同一URL时也进行分别对待的话,可以使用@vary_on_cookie装饰器。这一装饰器检测HTTP请求中Cookie头部的唯一性。

💡可以通过官方文档学习更多有关Django缓存框架的知识。同样也可以通过https://redis.io学习更多有关Redis的知识。

扩展知识…

虽然Redis可以以Memcached相同的方式处理缓存,它还有很多其它内置在系统中的缓存算法选项。除缓存外,Redis还可用作数据库或消息存储。它支持多种数据结构、事务、pub/sub(发布/订阅)和自动故障转移(automatic failover)等等。

借助django-redis-cache后台,还可以轻松地将Redis配置为会话后台,如下:

相关内容

退出移动版