自动转义HTML

当从模版中生成HTML文件时,总会存在各种风险,比如xss代码注入等恶意攻击。比如下面的模版片段:

Hello, {{ name }}

首先,它看起来像是无害的,用来显示用户的名字,但是设想一下,如果用户像下面这样输入他的名字,会发生什么:

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。
<script>alert('hello')</script>

使用这个名字的值,模版将会被渲染成这样:

Hello, <script>alert('hello')</script>

这意味着浏览器会弹出一个JavaScript警报框!

显然,用户提交的数据都被不应该被盲目的信任,并且被直接插入到网页中,因为一个怀有恶意的用户可能会使用这样的漏洞来做一些坏事。 这种类型的安全问题被叫做跨站脚本攻击(Cross Site Scripting)(XSS)。

为避免这个问题,有两个选择:

  • 第一,对每个不被信任的值运行escape过滤器,这将把潜在的有害的HTML字符转换成无害的字符串。在Django最初的几年里,这是默认的解决方案,但问题是它将责任放在开发人员/模板作者身上,以确保转义了所有内容,而且很容易忘记转义数据。
  • 第二,利用Django的自动HTML转义功能。默认情况下,Django中的每个模板会自动转义每个变量。也就是说,下面五个字符将被转义:
    • <会转换为<
    • >会转换为>
    • '(单引号)转换为'
    • "(双引号)会转换为"
    • &会转换为&

建议:将第二种功能做为默认打开的设置。

但是,有时,模板变量含有一些你打算渲染成原始HTML的数据,你并不想转义这些内容。 例如,你可能会在数据库中储存一些HTML代码,并且直接在模板中嵌入它们。有以下解决方法:

对于单个变量

使用safe过滤器来关闭变量上的自动转义:

This will be escaped: {{ data }}
This will not be escaped: {{ data|safe }}

对于模板块:

要控制模板上的自动转义,将模板(或者模板中的特定区域)包裹在autoescape标签中,像这样:

{% autoescape off %}
    Hello {{ name }}
{% endautoescape %}

autoescape标签接受on或者off作为它的参数。下面是一个模板的示例:

Auto-escaping is on by default. Hello {{ name }}

{% autoescape off %}
    This will not be auto-escaped: {{ data }}.

    Nor this: {{ other_data }}
    {% autoescape on %}
        Auto-escaping applies again: {{ name }}
    {% endautoescape %}
{% endautoescape %}

自动转义标签autoescape还会作用于扩展(extend)了当前模板的模板,以及通过include标签包含的模板,就像所有block标签那样。 看下面的例子:

# base.html文件

{% autoescape off %}
<h1>{% block title %}{% endblock %}</h1>
{% block content %}
{% endblock %}
{% endautoescape %}

# child.html文件

{% extends "base.html" %}
{% block title %}This &amp; that{% endblock %}
{% block content %}{{ greeting }}{% endblock %}

由于自动转义标签在base模板中关闭,它也会在child模板中关闭,导致当greeting变量含有Hello!字符串时,会渲染HTML。

过滤器的字符串参数:

过滤器的参数可以是字符串:

{{ data|default:"This is a string literal." }}

要注意,所有这种字符串参数在插入模板时都不会进行任何自动转义。模板的作者可以控制字符串字面值的内容,可以确保在模板编写时文本经过正确转义。即对自己传递的参数心里要有数。

{{ data|default:"3 &lt; 2" }}  # 正确的做法

{{ data|default:"3 < 2" }}     # 错误的做法

方法调用

大多数对象上的方法调用同样可用于模板中。这意味着模板能够访问到的不仅仅是对象的属性(比如字段名称)和视图中传入的变量,还可以执行对象的方法。例如:QuerySets提供了count()方法来计算含有对象的总数。因此,你可以像这样获取所有关于当前任务的评论总数:

{{ task.comment_set.all.count }}

还可以访问已经显式定义在模型上的方法:

# models.py
class Task(models.Model):
    def foo(self):
        return "bar"
template.html
{{ task.foo }}

由于Django有意限制了模板语言中的处理逻辑,不能够在模板中传递参数来调用方法。数据应该在视图中处理,然后传递给模板用于展示。

多对多调用

对于如下的模型:

from django.db import models

# Create your models here.

class Student(models.Model):
    name = models.CharField(max_length=128)

class Course(models.Model):
    name = models.CharField(max_length=128)
    students = models.ManyToManyField('Student')

模型Course有一个多对多字段指向Student模型。

正向查询

假设编写了一个如下的视图:

def test(request):
    course = models.Course.objects.get(pk=1)        #pk:有主键的字段
    return render(request, 'course.html', locals())

获取了id为1的course对象,并将它传递给course.html模版,模版代码如下:

{% for student in course.students.all %}

<p>{{ student.name }}</p>

{% endfor %}

首先通过course.students.all,查寻到course对象关联的students对象集,然后用for标签循环它,获取每个student对象,再用student模型的定义,访问其各个字段的属性。

反向查询

对于反向查询,从student往course查,假设有如下的视图:

def test2(request):
    student = models.Student.objects.get(pk=1)
    return render(request, 'student.html', locals())

获取了id为1的student对象,并将它传递给student.html模版,模版代码如下:

{% for course in  student.course_set.all %}
{{ course.name }}
{% endfor %}

通过student.course_set.all,反向获取到student实例对应的所有course对象,然后再for标签循环每个course,调用course的各种字段属性。

对于外键ForeignKey,其用法基本类似。只不过正向是obj.fk,且只有1个对像,不是集合。反向则是obj.fk_set,类似多对多。

定义标签和过滤器

Django为我们提供了自定义的机制,可以通过使用Python代码,自定义标签和过滤器来扩展模板引擎,然后使用{% load %}标签。

前置步骤

Django对于自定义标签和过滤器是有前置要求的,首先一条就是代码布局和文件组织:你可以为你的自定义标签和过滤器新开一个app,也可以在原有的某个app中添加。

  1. 在app中新建一个templatetags名字固定,不能变),和views.py、models.py等文件处于同一级别目录下。不要忘记创建__init__.py文件以使得该目录可以作为Python的

  2. 在添加templatetags包后,需要重新启动服务器,然后才能在模板中使用标签或过滤器。

  • 将你自定义的标签和过滤器将放在templatetags包下的一个模块里。
  • 这个模块的名字是后面载入标签时使用的标签名,所以要谨慎的选择名字以防与Django内置的或其他应用下的(自定义)标签和过滤器名字冲突。

假设你自定义的标签/过滤器在一个名为poll_extras.py的文件中:

polls/
    __init__.py
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

为了让{% load xxx %}标签正常工作,包含自定义标签的app必须在INSTALLED_APPS中注册。然后就可以在模板中像如下这样使用:

{% load poll_extras %}

在templatetags包中放多少个模块没有限制。{% load xxx %}将会载入给定模块名中的标签/过滤器,而不是app中所有的标签和过滤器。

要在模块内自定义标签,首先,这个模块必须包含一个名为register的变量,它是template.Library的一个实例,所有的标签和过滤器都是在其中注册的。 所以把如下的内容放在你的模块的顶部:

from django import template

register = template.Library()

友情提示:可以阅读Django的默认过滤器和标记的源代码。它们分别位于django/template/defaultfilters.pydjango/template/defaulttags.py中。它们是最好的范例。

自定义过滤器

自定义过滤器就是一个带有一个或两个参数的Python函数,这个Python函数的第一个参数要过滤的对象,第二个参数才是自定义的参数。而且最多总共只能有两个参数,所以你只能自定义一个参数。

  • 变量的值:不一定是字符串形式。
  • 参数的值:可以有一个初始值,或者完全不要这个参数。

由于模板语言没有提供异常处理,任何从过滤器中抛出的异常都将会显示为服务器错误

下面是一个定义过滤器的例子:

def cut(value, arg):
    """将value中的所有arg部分切除掉"""
    return value.replace(arg, '')

下面是这个过滤器的使用方法:

{{ somevariable|cut:"0" }}

大多数过滤器没有参数,但基本的value参数是必带的。例如:

def lower(value): # Only one argument.
    """Converts a string into all lowercase"""
    return value.lower()
注册过滤器

类原型:django.template.Library.filter()

好了过滤器函数,就需要注册它,有两种方法:

  1. 方法是调用register.filter,比如:
register.filter('cut', cut)
register.filter('lower', lower)

Library.filter()方法需要两个参数:

  • 过滤器的名称:一个字符串对象
  • 编译的函数 :编写的过滤器函数
  1. register.filter()用作装饰器,以如下的方式注册过滤器:
@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter        #没有声明name参数,Django将使用函数名作为过滤器的名字。
def lower(value):
    return value.lower()

自定义标签

Django提供了大量的快捷方式,使得编写标签比较容易。 对于一般的自定义标签来说,simple_tag是最重要的,它帮助你将一个Python函数注册为一个简单的模版标签。

simple_tag

原型:django.template.Library.simple_tag()

为了简单化模版标签的创建,Django提供一个辅助函数simple_tag,这个函数是django.template.Library的一个方法。

具体的编写方法:

  1. 在settings中的INSTALLED_APPS配置当前app,不然django无法找到自定义的simple_tag。

  2. 在app中创建templatetags模块(模块名只能是templatetags)。

  3. 创建任意 .py 文件,如:my_tags.py

  4. 在使用自定义simple_tag和filter的html文件中导入之前创建的 my_tags.py,{% load my_tags %} 

from django import template

register = template.Library()   #register的名字是固定的,不可改变

@register.filter
def filter_multi(v1,v2):
    return  v1 * v2

@register.simple_tag       # 和自定义filter类似,只不过接收更灵活的参数,没有个数限制。
def simple_tag_multi(v1,v2):
    return  v1 * v2


调用:
-------------------------------.html
{% load xxx %}  
# num=12
{{ num|filter_multi:2 }} # 24

{% simple_tag_multi num 5 %}  # 参数不限,但不能放在if、for语句中

注意:filter可以用在iffor等语句后,simple_tag不可以
{% if num|filter_multi:30 > 100 %}
    {{ num|filter_multi:30 }}
{% endif %}

比如,我们想编写一个返回当前时间的模版标签,那么current_time函数从而可以这样写︰

import datetime
from django import template

register = template.Library()

@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

关于simple_tag函数有几件值得注意的事项︰

如果不需要额外的转义,可以使用mark_safe()让输出不进行转义,前提是你绝对确保代码中不包含XSS漏洞。 如果要创建小型HTML片段,强烈建议使用format_html()而不是mark_safe()

from django import template
from django.utils.safestring import mark_safe

register = template.Library()   #register的名字是固定的,不可改变

@register.simple_tag
def my_input(id,arg):
    result = "<input type='text' id='%s' class='%s' />" %(id,arg,)
    return mark_safe(result)

如果模板标签需要访问当前上下文,可以在注册标签时使用takes_context参数︰

@register.simple_tag(takes_context=True)
def current_time(context, format_string):    # 一个参数必须称作context
    timezone = context['timezone']        
    return your_get_current_time_method(timezone, format_string)

如果你需要重命名你的标签,可以给它提供自定义的名称︰

register.simple_tag(lambda x: x - 1, name='minusone')

或:
@register.simple_tag(name='minustwo')
def some_function(value):
    return value - 2

simple_tag函数可以接受任意数量的位置参数和关键字参数

@register.simple_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

然后在模板中,可以将任意数量的由空格分隔的参数传递给模板标签。关键字参数的值使用等号("=")赋予,并且必须在位置参数之后提供。 例子:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

可以将标签结果存储在模板变量中,而不是直接输出。这是通过使用as参数后跟变量名来实现的:

{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>

渲染组件

inclusion_tag

原型:django.template.Library.inclusion_tag()

另一种常见类型的模板标签是通过渲染一个模板来显示一些数据。例如,Django的Admin界面使用自定义模板标签显示"添加/更改"表单页面底部的按钮。这些按钮看起来总是相同,但链接的目标却是根据正在编辑的对象而变化的。这种类型的标签被称为"Inclusion 标签",多用于返回html代码片段。

例:

#templatetags/inclusion_tag_test.py
from django import template

register = template.Library()

@register.inclusion_tag('result.html')  #将result.html里面的内容用下面函数的返回值渲染,然后作为一个组件一样,加载到使用这个函数的html文件里面。
def show_results(n):             # 参数可以传多个进来
    n = 1 if n < 1 else int(n)
    data = ["第{}项".format(i) for i in range(1, n+1)]
    return {"data": data}        # 这里可以传多个值。

# templates/snippets/result.html
<ul>
  {% for choice in data %}
    <li>{{ choice }}</li>
  {% endfor %}
</ul>

# templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>inclusion_tag test</title>
</head>
<body>
{% load inclusion_tag_test %}

{% show_results 10 %}  
</body>
</html>

注册有两种方法:

# 通过调用Library对象的inclusion_tag()装饰器方法创建并注册Inclusion标签︰
@register.inclusion_tag('results.html')     # results.html要渲染的html文件。
def show_results(poll):
    ...

# 也可以不用装饰器方法:django.template.Template实例注册Inclusion标签︰
from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)

inclusion_tag函数可以接受任意数量的位置参数和关键字参数。像这样:

@register.inclusion_tag('my_template.html')
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

然后在模板中,可以将任意数量的由空格分隔的参数传递给模板标签。关键字参数的值的设置使用等号("=") ,并且必须在位置参数之后提供。例如:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

可以在标签中传递上下文中的参数。比如说,当你想要将上下文context中的home_linkhome_title这两个变量传递给模版。 如下所示:

@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
    return {
        'link': context['home_link'],
        'title': context['home_title'],
    }

注意函数的第一个参数必须叫做context。context必须是一个字典类型。

register.inclusion_tag()这一行,我们指定了takes_context=True和模板的名字。模板link.html很简单,如下所示:

Jump directly to <a href="{{ link }}">{{ title }}</a>.

然后,当任何时候你想调用这个自定义的标签时,只需要load它本身,不需要添加任何参数,{{ link }}{{ title }}会自动从标签中获取参数值。像这样:

{% jump_link %}

使用takes_context=True,就表示不需要传递参数给这个模板标签。它会自己去获取上下文。

或者使用django.template.Template实例注册Inclusion标签︰

from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)

使用自定义标签和过滤器

某些应用提供了自定义的标签和过滤器。想要在模板中使用它们,首先要确保该应用已经在INSTALLED_APPS 中(比如在下面的例子中,添加了'django.contrib.humanize'),之后在模板中使用load标签:

{% load humanize %}
{{ 45000|intcomma }}

上面的例子中, load标签加载了humanizeapp的标签库,之后我们可以使用它的intcomma过滤器。

开启django.contrib.admindocs查询admin站点中的文档,便可以查看安装的自定义库列表。

load标签可以同时接受多个库名称,由空格分隔。 例如:

{% load humanize i18n %}

自定义库和模板继承:

当你加载一个自定义标签或过滤器库时,标签或过滤器只在当前模板中有效--并不是带有模板继承关系的任何父模板或者子模版中都有效。即在父模板中加载了自定义标签,子模版中还要再加载一次。

例如,如果一个模板foo.html带有{% load humanize %},子模版(例如,带有{% extends "foo.html" %})中不能访问humanize模板标签和过滤器。 子模版需要再添加自己的{% load humanize %}

这个特性是出于保持可维护性和逻辑性的目的。

配置模板引擎

模板引擎通过settings中的TEMPLATES设置来配置。列表中每个元素都是一个引擎配置字典。由startproject命令生成的settings.py会自定定义如下的值:

TEMPLATES = [
    {
        'BACKEND''django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS'True,
        'OPTIONS': {
            # ... some options here ...
        },
    },
]

# BACKEND:后端。内置的后端有django.template.backends.django.DjangoTemplates和django.template.backends.jinja2.Jinja2。

OPTIONS中包含了具体的后端设置。

由于绝大多数引擎都是从文件加载模板的,所以每种模板引擎都包含两项通用设置:

  • DIRS:定义了一个目录列表,模板引擎按列表顺序搜索这些目录以查找模板源文件。
  • APP_DIRS:告诉模板引擎是否应该进入每个已安装的应用中查找模板。通常将该选项保持为True。

每种模板引擎后端都定义了一个惯用的名称作为应用内部存放模板的子目录名称。(例如Django为它自己的模板引擎指定的是 ‘templates’,为jinja2指定的名字是‘jinja2’)。尤其是,django允许你有多个模板引擎后端实例,且每个实例有不同的配置选项。 在这种情况下必须为每个配置指定一个唯一的NAME 。

DTL引擎的OPTIONS配置项中接受以下参数:

  • autoescape:一个布尔值,用于控制是否启用HTML自动转义功能。默认为True。
  • context_processors: 以"."为分隔符的Python调用路径的列表。默认是个空列表。
  • debug:打开/关闭模板调试模式的布尔值。默认和setting中的DEBUG有相同的值。
  • loaders:模板加载器类的虚拟Python路径列表。默认值取决于DIRS和APP_DIRS的值。
  • string_if_invalid:非法变量时输出的字符串。默认为空字符串。
  • file_charset:用于读取磁盘上的模板文件的字符集编码。默认为FILE_CHARSET的值。
  • libraries:用于注册模板引擎。 这可以用于添加新的库或为现有库添加备用标签。
  • builtins:以圆点分隔的Python路径的列表。

简单的用法

django.template.loader中定义了两个函数以加载模板。

get_template(template_name,using = None)[source]

该函数使用给定的名称查找和加载模板,并返回一个Template对象。

模板的查找和加载机制取决于每种后端引擎和配置,如果想使用指定的模板引擎进行查找,请将模板引擎的NAME赋给get_template的using参数。

select_template(template_name_list,using = None)[source]

和get_template()相似, 只不过它使用包含模板名称的列表作为参数。

select_template()get_template()返回的Template对象都必须提供一个render()方法,如下所示:

Template.render(context=None, request=None)

通过给定的context对该模板进行渲染。

如果提供了context,那么它必须是一个dict对象。如果要提供request参数 ,必须使用HttpRequest对象。

针对下面的TEMPLATES配置,对模版文件的搜索顺序和路径如下:

TEMPLATES = [
    {
        'BACKEND''django.template.backends.django.DjangoTemplates',
        'DIRS': [
            '/home/html/example.com',
            '/home/html/default',
        ],
    },
    {
        'BACKEND''django.template.backends.jinja2.Jinja2',
        'DIRS': [
            '/home/html/jinja2',
        ],
    },
]

如果你调用函数get_template('story_detail.html'), Django将按以下顺序查找story_detail.html

/home/html/example.com/story_detail.html('django'引擎)
/home/html/default/story_detail.html('django'引擎)
/home/html/jinja2/story_detail.html('jinja2'引擎)

如果你调用函数select_template(['story_253_detail.html','story_detail.html']),Django按以下顺序查找:

/home/html/example.com/story_253_detail.html('django'引擎)
/home/html/default/story_253_detail.html('django'引擎)
/home/html/jinja2/story_253_detail.html('jinja2'引擎)
/home/html/example.com/story_detail.html('django'引擎)
/home/html/default/story_detail.html('django'引擎)
/home/html/jinja2/story_detail.html('jinja2'引擎)

注意:Django查找到任何一个匹配的模板后便停止搜寻,所以这是个类似url搜索的短路操作!

建议在每个APP的的模版子目录下都建立一个子目录来唯一对应这个APP。这样做可以增强APP的可用性。 将所有的模版文件放在根模版目录下会引发混淆。

要在一个子目录内加载模板,像下面这样:

get_template('news/story_detail.html')

如果结合上面例子中的TEMPLATES配置,这将会尝试按顺序查找并加载下列模板︰

/home/html/example.com/news/story_detail.html('django'引擎)
/home/html/default/news/story_detail.html('django'引擎)
/home/html/jinja2/news/story_detail.html('jinja2'引擎)

另外,为了减少加载模板、渲染模板等重复工作,django提供了处理这些工作的快捷函数。

render_to_string(template_name, context=None, request=None, using=None)[source]

render_to_string()会像get_template()一样加载模板并立即调用render()方法。 它需要以下参数。

  • TEMPLATE_NAME:要加载的模板的名称或列表。
  • context:要用作模板的上下文进行渲染的数据字典,也就是你要插入的动态数据字典。
  • request:可选的HttpRequest对象。
  • using:指定使用的模板引擎NAME。 搜索模板将仅限于该引擎。

用法示例:

from django.template.loader import render_to_string
rendered = render_to_string('my_template.html', {'foo''bar'})
扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄