第四章 Web表单
序:为什么需要Flask-wtf
第 2 章中介绍的请求对象包含客户端发出的所有请求信息。
其中, request.form 能获取POST 请求中提交的表单数据。
尽管 Flask 的请求对象提供的信息足够用于处理 Web 表单,
但有些任务很单调,而且要重复操作。
比如,生成表单的 HTML 代码和验证提交的表单数据。
Flask-WTF( http://pythonhosted.org/Flask-WTF/) 扩展可以把处理 Web 表单的过程变成一种愉悦的体验。
这个扩展对独立的 WTForms( http://wtforms.simplecodes.com)包进行了包装,方便集成到 Flask 程序中。
Flask-WTF 及其依赖可使用 pip 安装:
(venv) $ pip install flask-wtf
4.1 跨站请求伪造保护
为何需要CSRF保护
默认情况下, Flask-WTF 能保护所有表单免受跨站请求伪造
( Cross-Site Request Forgery,CSRF)的攻击。
恶意网站把请求发送到被攻击者已登录的其他网站时就会引发 CSRF 攻击。
为了实现 CSRF 保护,
Flask-WTF 需要程序设置一个密钥。
如何设置密钥
Flask-WTF 使用这个密钥生成加密令牌,
再用令牌验证请求中表单数据的真伪。
例:hello.py: 设置 Flask-WTF
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config 字典可用来存储框架、扩展和程序本身的配置变量。
使用标准的字典句法就能把配置值添加到 app.config 对象中。
这个对象还提供了一些方法,
可以从文件或环境中导入配置值。
SECRET_KEY 配置变量是通用密钥,
可在 Flask 和多个第三方扩展中使用。
如其名所示,加密的强度取决于变量值的机密程度。
不同的程序要使用不同的密钥,
而且要保证其他人不知道你所用的字符串。
为了增强安全性,密钥不应该直接写入代码,
而要保存在环境变量中。这一技术会在第 7 章介绍。
4.2 表单类
表单的结构
使用 Flask-WTF 时,
每个 Web 表单都由一个继承自 Form 的类表示。
这个类定义表单中的一组字段,
每个字段都用对象表示。
字段对象可附属一个或多个验证函数。
验证函数用来验证用户提交的输入值是否符合要求。
一个简单的web表单
from flask_wtf import FlaskForm # 0.13开始不推荐原书的Form
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired # 原书是Required,官网最新示例为DataRequired
class NameForm(Form):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
在这个示例中,
NameForm 表单中有一个名为 name 的文本字段
和一个名为 submit 的提交按钮。
WTForms支持的HTML标准字段
字段类型 | 说明 |
---|---|
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文本字段 |
DateField | 文本字段,值为 datetime.date 格式 |
DateTimeField | 文本字段,值为 datetime.datetime 格式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为 decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为 True 和 False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可选择多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
WTForms验证函数
验证函数 | 说明 |
---|---|
验证电子邮件地址 | |
EqualTo | 比较两个字段的值;常用于要求输入两次密码进行确认的情况 |
IPAddress | 验证 IPv4 网络地址 |
Length | 验证输入字符串的长度 |
NumberRange | 验证输入的值在数字范围内 |
Optional | 无输入值时跳过其他验证函数 |
Required | 确保字段中有数据 |
Regexp | 使用正则表达式验证输入值 |
URL | 验证 URL |
AnyOf | 确保输入值在可选值列表中 |
NoneOf | 确保输入值不在可选值列表中 |
4.3 把表单渲染成HTML
如何调用上节的NameForm表单
可以通过参数form传入模板,例如:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
<form>
标签是html语言用来显示表单的,
而缩进部分的form则是传入的参数,
是由hello.py中的render_temlate传进模板的。
如何渲染表单
这个表单还很简陋。要想改进表单的外观,
可以为字段指定 id 或 class 属性,
然后在CSS样式表里改变对应id或class的外观:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>
hidden_tag用来渲染所有的隐藏Field。
为什么使用Flask-Bootstrap渲染更好
即便能指定 id 或 class 属性,
但按照这种方式渲染表单的工作量还是很大,
所以在条件允许的情况下最好能使用 Bootstrap 中的预定义表单样式。
Flask-Bootstrap 使用预定义样式渲染整个 Flask-WTF 表单,
只需一次调用即可完成。
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
wtf.quick_form() 函数的参数为 Flask-WTF 表单对象。
使用Flask-Bootstrap的完整示例
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
模板的内容区(page_content)现在有两部分。
第一部分是页面头部(page_header),
第二部分使用 wtf.quick_form() 渲染上节的NameForm 实例。
这个程序必须和下节重定义的index()一起使用才行。
Jinja2 中的条件语句格式为 {% if condition %}...{% else %}...{% endif %}。
4.4 在视图函数中处理表单
更新后的index()视图函数
@app.route('/', methods=['GET', 'POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.data = ''
return render_template('index.html', form=form, name=name)
可以看到在路由中,多了一个methods,
后面除了默认值GET,还多了POST方法,
可以在网上搜下RESTFUL的简单说明。
NameForm的实例form会被Flask-Bootstrap渲染成网页上的表单,
只有当用户在表单中提交数据时,
才会执行if嵌套的语句,调用POST方法提交数据,
否则只是调用GET方法,显示空表单。
没必要把request.form(4.3第一个示例,form标签内容)传给Flask-wtf,
它自己会自动读取。
并且validate_on_submit会检查是否是POST,并且是否是有效数据。
因为每个表单都必须提交(submit),
所以validate_on_submit用来确认所有数据段的验证通过。
4.5 重定向和用户会话
POST后再刷新页面会出现警告
最新版的 hello.py 存在一个可用性问题。
用户输入名字后提交表单,
然后点击浏览器的刷新按钮,
会看到一个莫名其妙的警告,
要求在再次提交表单之前进行确认。
之所以出现这种情况,
是因为刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。
如果这个请求是一个包含表单数据的 POST 请求,
刷新页面后会再次提交表单。
大多数情况下,这并不是理想的处理方式。
如何避免刷新时POST作为最后一个请求:重定向
很多用户都不理解浏览器发出的这个警告。
基于这个原因,
最好别让 Web 程序把 POST请求作为浏览器发送的最后一个请求。
既然最后一个请求不能是POST方法,
可以尝试在POST后自动添加一个GET方法,
这个GET方法用原来的参数重新获取当前页面。
这种需求的实现方式是使用重定向,
会在把POST+GET封装为一个GET请求,
刷新命令也就能像预期那样使用了。
重定向时会丢失原有的输入数据
但这种方法会带来另一个问题。
程序处理 POST 请求时,
使用 form.name.data 获取用户输入的名字,
可是一旦这个请求结束,
数据也就丢失了。
如果没有用户输入的数据,
重定向后的页面就如同用户没有输入。
如何保存这些数据:用户会话
程序可以把数据存储在用户会话中,
在请求之间“ 记住”数据。
每个连接到服务器的客户端中都有不同的用户会话。
我们在第 2 章介绍过用户会话,
它是请求上下文中的变量,
名为 session,
像标准的 Python 字典一样操作。
默认情况下,
用户会话保存在客户端 cookie 中,
使用设置的 SECRET_KEY 进行加密签名。
如果篡改了 cookie 中的内容,
签名就会失效,会话也会随之失效。
使用重定向和用户会话的Index函数
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('/')
return render_template('index.html', form=form, name=session.get('name'))
新的Index函数以用户会话存储输入的数据,
然后重定向到当前URL,
再把存储好的数据发送给URL对应的模板。
即用session['name']
存储form.name.data
,
然后redirect到index对应的URL'/'
,
在把session['name']
发送给'/'
对应的模板。
推荐在重定向时,
使用redirect(url_for('index'))代替redirect('/'),
这样只要不改动index这个名字,
即使改动index对应的URL,
也可以正确地重定向。
form.name.data能够获取表单中name的值,
而session.get('name')则直接从会话中读取name的值。
使用session.get('name')而不是session['name']获取name的值,
可以避免发生未找到键的异常,
对于不存在的键,
get()会返回默认值None,
这点和普通字典一样。
4.6 Flash消息
为什么需要Flask消息
请求完成后,有时需要让用户知道状态发生了变化。
这里可以使用确认消息、警告或者错误提醒。
一个典型例子是,
用户提交了有一项错误的登录表单后,
服务器发回的响应重新渲染了登录表单,
并在表单上面显示一个消息,
提示用户用户名或密码错误。
这种功能是 Flask 的核心特性。
如下例所示,
flash() 函数可实现这种效果。
hello.py:Flash消息
from 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 消息,
因为这样所有页面都能使用这些消息。
Flask 把 get_flashed_messages() 函数开放给模板,
用来获取并渲染消息,如下例所示。
渲染 Flash 消息:templates/base.html
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
在模板中使用循环是因为在之前的请求循环中,
每次调用 flash() 函数时都会生成一个消息,
所以可能有多个消息在排队等待显示。
get_flashed_messages() 函数获取的消息在下次调用时不会再次返回,
因此 Flash 消息只显示一次,然后就消失了。
在这个示例中,
使用 Bootstrap 提供的警报 CSS 样式渲染警告消息。