Flask Web学习笔记 - 第4章 Web表单
尽管Flask的请求对象提供的信息足够用于处理Web表单,但有些任务很单调,而且要重复操作。比如,生成表单的HTML代码和验证提交的表单数据。
Flask-WTF(Flask - Web Toolkit Forms)是一个专为Flask框架设计的表单处理扩展,基于WTForms库构建。它简化了Web表单的创建、验证和安全防护,特别适合快速开发安全的用户交互功能(如登陆、注册、数据提交等)。
Flask-WTF及其依赖可使用pip安装:
(venv) $ pip install flask-wtf
4.1 跨站请求伪造保护
默认情况下,Flask-WTF能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery, CSRF)的攻击。
恶意网站把请求发送到被攻击者已登陆的其他网站时就会引发CSRF攻击。
为了实现CSRF,Flask-WTF需要程序设置一个密钥。Flask使用这个密钥生成加密令牌,再用加密令牌验证请求中表单数据的真伪(Flask-WTF使用密码加密令牌防止CSRF攻击)。
设置密钥的方法如下:
from flask import Flask
app = Flask(__name__) # 创建 Flask 实例
app.config['SECRET_KEY'] = 'hard to guess string' # 设置密钥
app.config字典可以用来存储框架、扩展和程序本身的配置变量。使用标准的字典句法就能把配置添加到app.config对象中。
SECRET_KEY配置变量是通用密钥,可在Flask和多个第三方扩展中使用。加密的强度取决于变量值的加密程度。
不同的程序要使用不同的密钥,而且要保证其他人不知道你所用的字符串。
注意,为了增强安全性,密钥不应该直接写入代码,而要保存在环境变量中。
4.2 表单类
使用Flask-WTF时,每个Web表单都由一个继承自Form的类表示。这个类定义表单中的一组字段,每个字段都用对象表示。
字段对象可附属一个或多个验证函数。
验证函数用来验证用户提交的输入值是否符合要求。
如以下示例是一个简单的Web表单,包含一个文本字段和一个提交按钮。
hello.py: 定义表单类
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
这个表单中的字段都定义为类变量,类变量的值是相应字段类型的对象。
在这个示例中,NameForm表单中有一个名为name的文本字段和一个名为submit的提交按钮。
StringField类表示属性为type=‘text’的元素。SubmitField类表示属性为type=‘submit’的元素。字段构造函数的第一个参数是把表单渲染成HTML时使用的标号。
StringField构造函数中的可选参数validators指定一个由验证函数组成的列表,在接受用户提交的数据之前验证数据。验证函数DataRequired()确保提交的字段不为空。
4.3 把表单渲染成HTML
Flask-Bootstrap提供了一个非常高端的辅助函数,可以使用Bootstrap中预先定义好的表单样式渲染整个Flask-WTF表单,而这些操作只需一次调用即可完成。
使用Flask-Bootstrap,表单可使用下面的方式渲染:
{% extends "bootstrap/wtf.html" as wtf%}
{
{ wtf.quick_form(form)}}
Import指令的使用方法和普通Python代码一样,允许导入模板中的元素并用在多个模板中。
导入的bootstrap/wtf.html文件中定义了一个使用Bootstrap渲染Flask-WTF表单对象的辅助函数。
wtf.quick_form()函数的参数为Flask-WTF表单对象,使用Bootstrap的默认样式渲染传入的表单。
如以下代码所示:
templates/index.html : 使用Flask-WTF和Flask-Bootstrap渲染表单
{% import "bootstrap/wtf.html" as wtf%}
{% block title %}Flasky - Index{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello,
{% if name %}
{
{ name }}
{% else %}
Stranger
{% endif %}!</h1>
{
{ wtf.quick_form(form) }}
</div>
{% endblock %}
模板的内容区现在由两部分。第一部分是页面头部,显示欢迎消息。这里用到了一个模板条件语句。Jinja2中的条件语句格式为{% if condition %}…{% else %}…{% endif %}。
如果条件的计算结果为True,那么渲染if和else指令之间的值;如果条件的计算结果为False,则渲染else和endif指令之间的值。
在这个例子中,如果没有定义模板变量name,则会渲染字符串“Hello, Stranger!”。
内容区的第二部分使用wtf.quick_form()函数渲染NameForm对象。
4.4 在视图函数中处理表单
在这一章节的hello.py中,视图函数index()不仅要渲染表单,还要接受表单中的数据。
如以下示例所示:
hello.py: 路由方法
@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', current_time=datetime.now(timezone.utc), form=form, name=name) # 渲染模板
app.route修饰器中添加的methods参数告诉Flask在URL映射中把这个视图函数注册为GET和POST请求的处理程序。
如果没指定methods参数,就只把视图函数注册为GET请求的处理程序。
把POST加入方法列表很有必要,因为提交表单作为POST请求进行处理更加便利。
表单也可作为GET请求提交,不过GET请求没有主体,提交的数据以查询字符串的形式附加到URL中,可以在浏览器的地址栏中看到。
基于这个及其他原因,提交表单大都作为POST请求处理。
局部变量name用来存放表单中输入的有效名字,如果没有输入,其值为None。如上述代码所示,在视图函数中创建一个NameForm()类示例用于表示表单。提交表单后,如果数据能被验证函数接受,那么validator_on_submit()方法的返回值为True,否则返回False。
这个函数的返回值决定是重新渲染表单还是处理表单提交的数据。
用户第一次访问程序时,服务器会收到一个没有表单数据的GET请求,所以validator_on_submit()将返回False。if语句的内容将被跳过,通过渲染模板处理请求,并传入表单对象和值None的name变量作为参数。用户会看到浏览器中显示了一个表单。
用户提交表单后,服务器收到一个包含数据的POST请求。validate_on_submit()会调用name字段上附属的DataRequired()验证函数。如果名字不为空,就能通过验证,validate_on_submit()返回True。用户输入的名字可通过字段的data属性获取。在if语句中,把名字赋值给局部变量name,然后再把data属性设置为空字符串,从而清空表单字段。最后一行调用render_template()函数渲染模板,但这一次参数name的值为表单中输入的名字,因此会显示一个针对该用户的欢迎消息。
如果用户提交表单之前没有输入名字,DataRequired()验证函数会捕获这个错误,如下图所示:
完整代码如下所示:
hello.py:
from flask import Flask, render_template
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from datetime import datetime, timezone # 导入 datetime 模块
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
app = Flask(__name__) # 创建 Flask 实例
bootstrap = Bootstrap(app) # 初始化 Bootstrap
moment = Moment(app) # 初始化 Moment
app.config['SECRET_KEY'] = 'hard to guess string' # 设置密钥
@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', current_time=datetime.now(timezone.utc), form=form, name=name) # 渲染模板
@app.route('/user/<name>') # 定义路由
def user(name):
return render_template('user.html', name=name) # 渲染模板
@app.errorhandler(404) # 定义路由
def page_not_found(e):
return render_template('404.html'), 404 # 渲染模板
@app.errorhandler(500) # 定义路由
def internal_server_error(e):
return render_template('500.html'), 500 # 渲染模板
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
if __name__ == '__main__':
app.run(debug=True) # 启动 Flask 服务器,开启调试模式
templates/index.html:
{
% extends "base.html" %}
{
% import "bootstrap/wtf.html" as wtf %}
{
% block title %}Flasky - Index{
% endblock %}
{
% block scripts %}
{
{
super() }}
{
{
moment.include_moment() }}
{
{
moment.locale('zh-cn') }}
{
% endblock %}
{
% block page_content %}
<div class="page-header">
<h1>Hello,
{
% if name %}
{
{
name }}
{
% else %}
Stranger
{
% endif %}!</h1>
<p>当地时间是{
{
moment(current_time).format('LLL') }},即是{
{
moment(current_time).fromNow(refresh=True) }}.</p>
</div>
<div class="page_content">
{
{
wtf.quick_form(form) }}
</div>
{
% endblock %}
4.5 重定向和用户会话
Hello.py存在一个可用性问题。用户输入名字后提交表单,然后点击浏览器的刷新按钮,会看到一个莫名其妙的警告,要求在再次提交表单之前进行确认。
如下图所示:
之所以出现这种情况,是因为刷新页面时点击浏览器会重新发送之前已经发送过的最后一个请求。如果这个请求是一个包含表单数据的POST请求,刷新页面后会再次提交表单,而这并不是理想的处理方式。
很多用户都不理解浏览器发出的这个警告,基于这个原因,最好别让Web程序把POST请求作为浏览器发送的最后一个请求。这种需求的实现方式是使用重定向作为POST请求的响应,而不是使用常规响应。
如以下示例代码实现了重定向和用户会话:
hello.py:
from flask import Flask, render_template, session, redirect, url_for # 导入 Flask 类
@app.route('/', methods=['GET', 'POST']) # 定义路由
def index():
name = None
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', current_time=datetime.now(timezone.utc), form=form, name=session.get('name')) # 渲染模板
重定向是一种特殊的响应,响应内容是URL,而不是包含HTML代码的字符串。浏览器收到这种响应时,会向重定向的URL发起GET请求,显示页面的内容。
这个页面的加载可能要多花几微秒,因为要先把第二个请求发送给服务器。现在最后一个请求是GET请求,所以刷新命令能像预期那样正常使用。
这个技巧称为POST/重定向/Get模式。
但这种方法会带来另一个问题。程序处理POST请求时,使用form.name.data获取用户输入的名字,一旦这个请求结束,数据也就丢失了。因为这个POST请求使用重定向处理,所以程序需要保存输入的名字,这样重定向后的请求才能获得并使用这个名字,从而构建真正的响应。
程序可以把数据存储在用户会话中,在请求之间使用用户会话“记住”数据。
用户会话是一种私有存储,存在于每个连接到服务器的客户端中。它是请求上下文中的变量,名为session,像标准的Python字典一样地操作。
默认情况下,用户会话存在客户端cookie中,使用设置的SECRET_KEY进行加密签名。如果篡改了cookie中的内容,签名就会失效,用户会话也会随之失效。
在程序前一版中,局部变量name被用于存储用户在表单中输入的名字。这个变量现在保存在用户会话中,即session[‘name’],所以在两次请求之间也能记住输入的值。
现在包含合法表单数据的请求最后会调用redirect()函数。
redirect()函数是个辅助函数,用来生成HTTP重定向响应。redirect()函数的参数是重定向的URL,这里使用的重定向URL是程序的根地址,因此重定向响应文本可以写成redirect(‘/’),但却会使用Flask提供的URL生成函数url_for()。
url_for()函数的第一个且唯一必须指定的参数是端点名,即路由端点的相应视图函数名字(在这个示例中,是index()函数)。
最后一处是在render_template()函数中,使用session.get(‘name’)直接从用户会话中读取name参数的值。对于不存在的键,get()会返回默认值None。
使用这个版本的程序时,刷新浏览器页面,看到的新页面就会和预期一样了。
4.6 Flash消息
请求完成后,有时需要让用户知道状态发生了变化。
这里可以使用确认消息、警告或者错误提醒。一个典型的例子是,用户提交了有一项错误的登录表单后,服务器发回的响应重新渲染了登陆表单,并在表单上显示消息,提示用户用户名或者密码错误。
这种功能是Flask的核心特性,flash()函数可以实现这种效果。
如以下示例所展示:
hello.py
from flask import Flask, render_template, session, redirect, url_for, flash # 导入 Flask 类
@app.route('/', methods=['GET', 'POST']) # 定义路由
def index():
name = None
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', current_time=datetime.now(timezone.utc), form=form, name=session.get('name')) # 渲染模板
在这个示例中,每次提交的名字都会和存储在用户会话中的名字进行比较,而会话中存储的名字是前一次在这个表单中提交的数据。如果两个名字不一样,就会调用flash()函数,在发给客户端的下一个响应中显示一个消息。
仅调用flash()函数并不能把消息显示出来,程序使用的模板要渲染这些消息。最好在基模板中渲染Flash()消息,因为这样所有页面都能使用这些消息。
Flash把get_flashed_messages()函数开放给模板,用来获取并渲染消息。如以下示例所展示:
template/base.html: 渲染Flash消息
{% 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 %}
在这个示例中,使用Bootstrap提供的警报CSS样式渲染警告消息。如下图所示:
在模板中使用循环是因为在之前的请求循环中每次调用flash()函数时都会生成一个消息,所以可能有多个消息在排队等待显示。
get_flashed_message()函数获取的消息在下次调用时不会再次返回,因此Flash消息只显示一次,然后就消失了。