4.1 跨站请求伪造保护

跨站请求伪造(CSRF):恶意网站吧请求发送到,被攻击者已登录的其他网站。
flask-wtf需要程序设置一个密匙。然后利用这个密匙生成加密令牌,再用令牌验证请求中表单数据的真伪,从而实现CSRF保护

设置flask-wtf

1
2
3
# ...
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

注意:为了增强安全性,密匙不应该直接写入代码中,而要保存环境变量中(第七章介绍)。

4.2 定义表单类

使用flask-wtf时,每个Web表单都继承自Form类。这个类定义表单中的一组字段,每组字段都用对象(字段类的实例对象,如StringField(‘hello’))表示。字段对象可以附有一个或多个验证函数
验证函数(validator):用来验证用户提交的数据是否符合要求。

定义名为NameForm的表单类:

1
2
3
4
5
6
7
from flask-wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
class NameForm(Form):
name = StringField('What is your name?', validators=[Required()])
submit = SubmitField('Submit')

NameForm表单中的字段定义为类变量,
类变量的值为对应字段的对象。

字段构造函数(如SubmitField()):的第一个参数是把表单渲染成HTML时使用的标号(或文本)。

validators参数:一个由验证函数组成的列表

表4-1 WTFORMS支持的HTML标准字段

字段类型 说明
StringField 文本字段
TextAreaField 多行文本字段
PasswordField 密码文本字段

表4-2 WTFORMS验证函数

验证函数 说明
Email 验证电子邮件地址
Required 确保字段中有数据
AnyOf 确保输入值在可选值列表中

4.3 把表单渲染成HTML

假设视图函数把一个NameForm实例(通过参数form)传给模板,那么在模板中可以生成一个简单的表单:

1
2
3
4
5
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label}} {{ form.name() }}
{{ form.submit() }}
</form>

为了改进外观,可以将HTML属性(如id、class等)传给渲染字段的函数,如:

1
2
3
4
5
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label}} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>

flask-bootstrap提供了一个辅助函数涌来渲染flask-wtf表单。用法如下:

1
2
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}

bootstrap/wtf.html文件中定义了一个用于渲染Flask-WTF表单对象的辅助函数。

wtf.qucik_form()函数的参数为Flask-WTF表单对象(表单类的实例)。

4.4 在视图函数中处理表单

1
2
3
4
5
6
7
8
9
10
# ...
@app.route('/', methods=['GET', 'POST']
def index():
name = None
form = NameForm() # 实例化表单类
if form.validate_on_submit():
name = form.name.data
form.name.data = '' # 将form.name.data的值设置为空字符串
return render_template('index.html', form=form, name=name)

methods参数告诉Flask在URL映射中把这个视图函数注册为GETPOST请求的处理程序。如果没有指定methods参数,就只把视图函数注册为GET请求的处理程序。

validate_on_submit():提交表单后,如果数据能被全部验证函数接受,那么validate_on_submit()的返回值为True,否则返回False

4.5 重定向和用户会话

现在的hello.py存在一个问题:用户输入名字后提交表单,然后刷新页面,会出现一个警告(要求在再次提交表单前进行确认)。
出现这种情况的原因是:刷新页面时,浏览器会重新发送之前已经发送过的最后一个请求。如果这个请求是一个包含表单数据的POST请求,刷新页面会再次提交表单。

解决思路:使浏览器发送的最后一个请求不是POST请求。
解决方法:使用重定向作为POST请求的响应,而不是使用常规响应。

重定向:是一个特殊响应,响应的内容是URL,而不是包含HTML代码的字符串。浏览器收到这种响应时,会向重定向的URL发起GET请求(第二个请求),显示页面内容。

存在问题:这种方法会带来另一个问题,程序处理POST请求时,使用form.name.data来获取用户输入值,可是一旦这个请求结束,数据也就丢失(因为这个POST请求使用重定向作为响应)。所以程序需要保存输入值,方便重定向后的请求可以获得并使用这个值。

解决方法2 :把数据存储在用户会话中。

使用重定向和用户会话重写hello.py

1
2
3
4
5
6
7
8
9
10
from flask import Flask, render_template, session, redirect, url_for
# ...
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'))

使用session.get()方法获取字典中键对应的值,避免未找到键的异常情况。因为对于不存在的键, get()会返回默认值None

4.6 Flash消息

请求完成后,有时需要让用户知道状态发生了变化(如使用确认消息、警告或错误提醒)。一个典型例子是,提交了一项有错误的登录表单后,服务器发回的响应重新渲染登录表单,并且在表单上面显示一个消息,提示用户名或密码错误。

设置Flash消息的hello.py

1
2
3
4
5
6
7
8
9
10
11
12
13
form flask import Flask, render_template, session, redirect, url_for, flash
# ...
@app.route('/', methods=['GET', 'POST']
def index():
form = NameForm()
if form.validate_on_submit():
old_name = session.get('name')
if old_name is not None and old_name != form.name.data:
flash('Looks like you have changed your name!')
session['name'] = form.name.data
return redirect(url_for('index')
return render_template('index.html', form=form, name=session.get('name'))

在这个示例中,将用户提交的数据和用户会话中的数据比较,如果两个数据不一样,就会调用flash()函数,在发给客户端的下一个响应中显示一个信息

在模板中渲染Flash消息:仅调用flash()函数并不能将消息显示出来,这个可以在模板中渲染Flash消息。如base.html

1
2
3
4
5
6
7
8
9
10
11
12
13
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">
&times;
</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %} {% endblock %}
</div>
{% endblock %}

get_flashed_messages()函数用来获取并渲染Flash消息。

注意

  1. 在模板中使用for循环是因为,每次调用falsh()函数时,都会生产一个消息,所以可能有多个消息在排队等待显示,所需需要用for循环将消息都显示出来。
  2. get_flashed_messages()函数获取的消息在下次调用时不会再次返回(如这次获取了消息队列1,那么在下次调用时就不会再返回消息队列1,有点类似于列表的pop()方法),因此Flash消息只显示一次,然后就消失了。