1. 模板简介
2. 模板语言 DTL
3. 模板继承
4. HTML 转义
5. CSRF
1. 模板简介
作为 Web 开发框架,Django 提供了模板,可以很便利的动态生成 HTML。模版系统致力于表达外观,而不是程序逻辑。
模板的设计实现了业务逻辑(view)与显示内容(template)的分离,一个视图可以使用任意一个模板,一个模板可以供多个视图使用。
模板包含:
- HTML的静态内容
- 动态插入的内容(Django 模板语言,简写 DTL,定义在 django.template 包中)
由 startproject 命令生成的 settings.py 定义关于模板的值:
- DIRS 定义了一个目录列表,模板引擎按列表顺序搜索这些目录以查找模板源文件。
- APP_DIRS 告诉模板引擎是否应该在每个已安装的应用中查找模板。
常用方式:在项目的根目录下创建 templates 目录,设置 DIRS 值。
DIRS = [os.path.join(BASE_DIR,"templates")]
模板处理
Django 处理模板分为两个阶段:
- Step1 加载:根据给定的标识找到模板然后预处理,通常会将它编译好放在内存中
loader.get_template(template_name) # 返回一个Template对象
- Step2 渲染:使用Context数据对模板插值并返回生成的字符串
Template对象的render(RequestContext)方法,使用context渲染模板
- 加载渲染完整代码:
from django.template import loader, RequestContext
from django.http import HttpResponse def index(request):
tem = loader.get_template('temtest/index.html')
context = RequestContext(request, {})
return HttpResponse(tem.render(context))
快捷函数
为了减少加载模板、渲染模板的重复代码,Django 提供了快捷函数:
- render_to_string("")
- render(request, '模板', context)
from django.shortcuts import render def index(request):
return render(request, 'temtest/index.html')
2. 模板语言 DTL
模板语言包括:
- 变量 {{ 变量名 }}
- 标签 { % 代码块 % }
- 过滤器
- 注释 {# 代码或html #}
变量
语法:
{{ variable }}
- 当模版引擎遇到一个变量,将计算这个变量,然后将结果输出。
- 变量名只能由字母、数字、下划线(不能以下划线开头)和点组成。
- 当模版引擎遇到点("."),会按照下列顺序查询:
- 字典查询,例如:foo["bar"]
- 属性或方法查询,例如:foo.bar
- 数字索引查询,例如:foo[bar]
- 如果变量不存在, 模版系统将插入空字符串。
- 在模板中调用方法时不能传递参数。
范例:在模板中调用对象的方法
- 在 models.py 中定义类 HeroInfo 类:
from django.db import models class HeroInfo(models.Model):
...
def showName(self):
return self.hname
- 在 views.py 中传递 HeroInfo 对象:
from django.shortcuts import render
from models import * def index(request):
hero = HeroInfo(hname='abc')
context = {'hero': hero}
return render(request, 'temtest/detail.html', context)
- 在模板 detail.html 中调用对象的方法:
{{hero.showName}}
标签
语法:
{% 代码块 %}
作用:
- 在输出中创建文本
- 控制循环或逻辑
- 加载外部信息到模板,供以后的变量使用
for 标签
语法:
{% for ... in ... %}
# 循环体中的逻辑
{{forloop.counter}} # 表示当前是第几次循环(从1开始)
{% empty %}
# 给出的列表为空或列表不存在时,执行此处。类似于 else
{% endfor %} # for 循环的结束标识
示例:
<body>
{% for hero_obj in hero %}
{{ forloop.counter }}: {{hero_obj.show}}<br/>
{% empty %}
<h2>啥也没找到...</h2>
{% endfor %}
</body>
1: 郭靖
2: 黄蓉
3: 比伯
4: 王嘉尔
5: 欧阳锋
if 标签
语法:
{% if ... %}
逻辑1
{% elif ... %}
逻辑2
{% else %}
逻辑3
{% endif %}
示例:
<body>
<ul>
{% for hero_obj in hero %}
{% if forloop.counter|divisibleby:"2" %} {# 当前行数是否能整除2:偶数行为蓝色,奇数行为红色 #}
<li style="color: blue">{{ forloop.counter }}: {{hero_obj.show}}</li>
{% else %}
<li style="color: red">{{ forloop.counter }}: {{hero_obj.show}}</li>
{% endif %}
{% empty %}
<h2>啥也没找到...</h2>
{% endfor %}
</ul>
</body>
include 标签
加载模板并以标签内的参数渲染:
{% include "foo/bar.html" %}
url 标签
反向解析:在模板中的链接部分不使用硬编码,而是使用反向解析根据 urls 配置生成访问地址。
原先的访问方式:根据访问地址去匹配 urls。
范例 1:不带参数
项目 urls.py:
url(r'^hero_book/', include('hero_book.urls', namespace='hero_book')),
应用 urls.py:
url(r"^\d+$", views.show, name='show'),
views.py:
def show(request):
return render(request, "hero_book/show.html")
模板 index.html:
<a href="{%url 'hero_book:show' %}">show</a> # 根据 urls 配置自动生成符合 hero_book/\d+ 的访问地址
模板 show.html:
<body>
show page
</body>
执行效果:
范例 2:带参数
应用 urls.py:
url(r"^(\d+)$", views.show, name='show'), # 接收反向解析中传递过来的参数
模板 index.html:
<a href="{%url 'hero_book:show' 123 %}">show</a> {# 传递数字123作为参数 #}
模板 show.html:
show page {{ id }}
执行效果:
csrf_token 标签
跨站请求伪造保护:
{% csrf_token %}
布尔标签
- and、or
- and 比 or 的优先级高
注释
- 多行注释:comment 标签
{% comment %}
多行注释
{% endcomment %}
- 单行注释:
{# ... #}
- 注释可以包含任何模版代码,有效的或者无效的都可以:
{# { % if foo % }bar{ % else % } #}
过滤器
语法:
{{ 变量|过滤器 }}
- 使用管道符号 | 来应用过滤器。
- 通过使用过滤器来改变变量的计算结果。
- 可以在 if 标签中使用过滤器结合运算符。
示例:
{% if list1|lenth > 1 %} # 判断变量 list1 的长度是否大于1
{{ name|lower }} # 表示将变量 name 的值变为小写输出
- 过滤器能够被“串联”,构成过滤器链:
name|lower|upper
- 过滤器可以传递参数,参数使用引号包起来
list|join:", "
- default:如果一个变量没有被提供、值为 false 或空,则使用默认值;否则使用变量的值:
value|default:"什么也没有"
- date:根据给定格式对一个 date 变量格式化:
value|date:'Y-m-d'
3. 模板继承
模板继承可以减少页面内容的重复定义,实现页面内容的重用。
典型应用:网站的头部、尾部是一样的,这些内容可以定义在父模板中,子模板不需要重复定义。
语法
- block 标签:在父模板中预留区域,在子模板中填充。
{% block block_name %}
这里可以定义默认值
如果不定义默认值,则表示空字符串
{ %endblock%}
- extends 标签:在子模板中继承父模板,写在模板文件的第一行。
{% extends "父模板名称" %}
- 在子模板中使用 block 填充预留区域:
{% block block_name %}
实际填充内容
{% endblock %}
- 为了更好的可读性,可以给 endblock 标签一个名字:
{% block block_name %}
区域内容
{% endblock block_name %}
使用说明
- 如果在模版中使用 extends 标签,它必须是模版中的第一个标签。
- 不能在一个模版中定义多个相同名字的 block 标签。
- 子模版不必定义全部父模版中的 blocks,如果子模版没有定义 block,则使用父模版中的默认值。
- 如果发现在模板中有大量的可重用内容,就应该把内容移动到父模板中。
范例:二层继承
views.py:
def child(request):
return render(request, "hero_book/child.html")
urls.py:
url(r"^child/$", views.child, name="child"),
定义父模板 base.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
{% block head %}
{% endblock %}
</head>
<body>
{% block content1 %}
<h1>content1</h1> {# content1的默认值 #}
{% endblock %}
<hr/>
{% block content2 %}
<h1>content2</h1> {# content2的默认值 #}
{% endblock %}
</body>
</html>
效果 1:子模板 child.html 只继承,不填充。
{% extends "hero_book/base.html" %}
效果 2:子模板 child.html 继承并填充。
{% extends "hero_book/base.html" %} {% block content1 %} {# 替换父模板中的默认值 #}
<h1>child content1</h1>
{% endblock content1 %}
范例:三层继承
三层继承结构使代码得到最大程度的复用,并且使得添加内容更加简单。
如下图为常见的电商页面:
实现示例:用户页
定义 base.html,重用页面的头尾内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
{% block head %}
{% endblock %}
</head>
<body>
<h1>TOP</h1>
<hr/>
{% block content %}
{% endblock content %}
<hr/>
<h1>BOTTOM</h1>
</body>
</html>
定义 user_base.html 继承 base.html,重用用户页的布局和左侧导航:
{% extends "hero_book/base.html" %} {% block content %}
<table border="1">
<tr>
<td height="300">用户</br>导航</td>
<td>{% block user_content %}{% endblock %}</td>
</tr>
</table>
{% endblock content %}
定义 user1.html 和 user2.html 分别继承 user_base.html,展示具体的用户信息:
{% extends "hero_book/user_base.html" %} {% block user_content %}
用户信息1
{% endblock user_content %}
{% extends "hero_book/user_base.html" %} {% block user_content %}
用户信息2
{% endblock user_content %}
views.py:
def user1(request):
return render(request, "hero_book/user1.html") def user2(request):
return render(request, "hero_book/user2.html")
urls.py:
url(r"^user1/$", views.user1, name="user1"),
url(r"^user2/$", views.user2, name="user2"),
执行效果:
4. HTML 转义
Django 会对视图函数传递的字符串自动进行 HTML 转义,如在模板中输出如下值:
# 视图代码
def index(request):
return render(request, 'temtest/index2.html', {'t1': '<h1>hello</h1>'}) # 模板代码
{{t1}}
网页显示效果如下:
<h1>hello</h1>
会被自动转义的字符
HTML 转义,就是将包含的 HTML 标签输出,而不被解释执行。原因是当显示用户提交字符串时,可能包含一些攻击性的代码,如 js 脚本。
Django 会将如下字符自动转义:
< 会转换为 < > 会转换为 > ' (单引号) 会转换为 ' " (双引号) 会转换为 " & 会转换为 &
当显示不被信任的变量时使用 escape 过滤器,一般省略,因为 Django 自动转义:
{{ t1|escape }}
关闭转义
对于变量使用 safe 过滤器:
{{ data|safe }}
对于代码块使用 autoescape 标签:
{% autoescape off %}
{{ body }}
{% endautoescape %}
- 标签 autoescape 接受 on 或者 off 参数。
- 自动转义标签如果在 base 模板中关闭,那么在 child 模板中也是关闭的。
字符串字面值
以下代码会解释输出加粗的 123:
{{ data|default:"<b>123</b>" }}
手动转义应写为:
{{ data|default:"<b>123</b>" }}
5. CSRF
跨站请求伪造(CSRF:Cross-site request forgery),简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
例子
假如一家银行用以运行转账操作的 URL 地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码: <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。
CSRF 演示
创建视图 csrf1 用于展示表单,csrf2 用于接收 post 请求:
def csrf1(request):
return render(request, 'hero_book/csrf1.html') def csrf2(request):
uname = request.POST['uname']
return render(request, 'hero_book/csrf2.html', {'uname': uname})
配置 url:
url(r'^csrf1/$', views.csrf1),
url(r'^csrf2/$', views.csrf2),
csrf1.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" action="/hero_book/csrf2/">
<input name="uname"><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
csrf2.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
{{ uname }}
</body>
</html>
在浏览器中查看访问效果,报错如下:
- 原因: Django 自带了防 CSRF 的中间件,需注释掉该功能才能访问(只要是 POST 请求均有此限制)。
- 解决方法:将 settings.py 中的中间件代码 'django.middleware.csrf.CsrfViewMiddleware' 注释掉,即可正常提交 POST 请求。
此时产生会产生 CSRF 问题:
查看 csrf1 的源代码,复制粘贴成本地的另一个 HTML 文件,访问查看效果:正常访问。
防 CSRF 的使用
在 Django 的模板中,提供了防止跨站攻击的方法,使用步骤如下:
- step1:在 settings.py 中启用 'django.middleware.csrf.CsrfViewMiddleware' 中间件(此项在创建项目时,默认被启用)。
- step2:在csrf1.html 中添加标签:
<form method="post" action="/hero_book/csrf2/">
{% csrf_token %}
<input name="uname">
<input type="submit" value="提交"/>
</form>
- step3:测试刚才的两个请求,发现 csrf1.html 正常提交 POST 请求,而跨站(本地 HTML 文件)的请求被拒绝了,效果如下图:
取消保护
如果某些视图不需要保护,可以使用装饰器 @csrf_exempt,模板中也不需要写标签,修改 csrf2 的视图如下:
from django.views.decorators.csrf import csrf_exempt @csrf_exempt
def csrf2(request):
uname = request.POST['uname']
return render(request, 'hero_bookt/csrf2.html', {'uname': uname})
再次运行之前的两个请求,发现都可以请求。
保护原理
加入 {% csrf_token %} 标签后,可以查看源代码,发现多了如下代码:
<input type='hidden' name='csrfmiddlewaretoken' value='nGjAB3Md9ZSb4NmG1sXDolPmh3bR2g59' />
在浏览器的调试工具中,通过 network 标签可以查看 cookie 信息,发现本站中自动添加了 cookie 信息,如下图:
当提交 POST 请求时,中间件 'django.middleware.csrf.CsrfViewMiddleware' 会对提交的 cookie 及隐藏域的内容进行验证,如果失败则返回 403 错误。
查看跨站的信息,并没有 cookie 信息,但当加入上面的隐藏域代码,发现又可以访问了。结论:Django 的 CSRF 不是完全的安全。