在第2章中,我为应用程序的主页创建了一个简单的模板,并使用伪造的对象作为占位符来处理我还没有的东西,例如用户或博客文章。 在本章中,我将解决该应用程序中仍然存在的众多漏洞之一,特别是如何通过Web表单接受用户的输入。
Web表单是任何Web应用程序中最基本的构建块之一。 我将使用表格允许用户提交博客文章,以及登录到应用程序。
在继续本章之前,请确保已安装上一章中所述的微博客应用程序,并且可以正确运行。
为了处理此应用程序中的Web表单,我将使用Flask-WTF扩展,它是WTForms包的包装,可以很好地将其与Flask集成在一起。 这是我要向你介绍的第一个Flask扩展程序,但不会是最后一个。 扩展是Flask生态系统中非常重要的组成部分,因为扩展为Flask有意不接受的问题提供了解决方案。
Flask扩展是随pip一起安装的常规Python软件包。 你可以继续在虚拟环境中安装Flask-WTF:
(venv) $ pip install flask-wtf
到目前为止,该应用程序非常简单,因此,我无需担心其配置。 但是对于除最简单的应用程序之外的任何应用程序,你都会发现Flask(以及可能使用的Flask扩展)在做事方面提供了一定的自由度,你需要做出一些决定,并将其传递给 框架作为配置变量列表。
应用程序有几种格式可以指定配置选项。 最基本的解决方案是将变量定义为app.config
中的键,该键使用字典样式来处理变量。 例如,你可以执行以下操作:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed
尽管上面的语法足以为Flask创建配置选项,但我想强制执行关注点分离的原理,因此,与其将我的配置放在创建应用程序的同一位置,我将使用稍微复杂一些的结构, 将我的配置保存在单独的文件中。
我非常喜欢的一种格式(因为它具有很好的可扩展性)是使用一个类来存储配置变量。 为了使事情井井有条,我将在单独的Python模块中创建配置类。 在下面,你可以看到此应用程序的新配置类,该类存储在顶层目录中的config.py
模块中。
#config.py: Secret key configuration
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
很简单,对吧?配置设置被定义为Config
类中的类变量。由于应用程序需要更多配置项,因此可以将它们添加到此类中,稍后,如果我发现我需要多个配置集,则可以为其创建子类。但是现在不用担心。
我添加为唯一配置项的SECRET_KEY
配置变量是大多数Flask应用程序中的重要组成部分。 Flask及其某些扩展使用秘密密钥的值作为加密密钥,可用于生成签名或令牌。 Flask-WTF扩展程序使用它来保护Web表单免受称为“跨站点请求伪造”或CSRF(发音为“ seasurf”)的讨厌攻击。顾名思义,秘密密钥被认为是秘密的,因为由此产生的令牌和签名的强度不依赖于应用程序的受信任维护者之外的任何人都知道。
密钥的值设置为带有两个术语的表达式,由或(or
)运算符连接。第一项寻找环境变量的值,也称为SECRET_KEY
。第二项,只是一个硬编码的字符串。你会看到我经常重复这种配置变量模式。这个想法是最好使用来自环境变量的值,但是如果环境未定义该变量,那么将使用硬编码的字符串。开发此应用程序时,安全性要求很低,因此你可以忽略此设置并使用硬编码的字符串。但是,当将此应用程序部署在生产服务器上时,我将在环境中设置一个唯一且难以猜测的值,以便该服务器具有其他人都不知道的安全密钥。
现在我有了一个配置文件,我需要告诉Flask读取并应用它。这可以在使用app.config.from_object()
方法创建Flask应用程序实例之后立即完成:
app/__init__.py: Flask configuration
from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
首先,我导入Config
类的方式似乎令人困惑,但是如果你查看Flask
类(大写“ F”)如何从flask
包(小写“ f”)中导入,你会发现我对配置进行相同的操作。 小写的config
是Python模块config.py
的名称,显然带有大写“ C”的是实际的类。
如上所述,可以使用app.config
中的字典语法访问配置项。 在这里,你可以看到与Python解释器的快速会话,在其中我检查密钥的值是什么:
>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'
Flask-WTF扩展使用Python类来表示Web表单。 表单类仅将表单的字段定义为类变量。
再次考虑到关注点分离,我将使用一个新的app/forms.py
模块来存储我的Web表单类。 首先,让我们定义一个用户登录表单,要求用户输入用户名和密码。 该表格还将包括一个“记住我”(Remember Me)复选框和一个"Sign In"提交按钮:
#app/forms.py: Login form
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
大多数Flask扩展都使用flask_<name>
命名约定作为其顶级导入符号。 在这种情况下,Flask-WTF的所有符号都在flask_wtf
下。 这是从app/forms.py
顶部导入FlaskForm
基类的位置。
因为Flask-WTF扩展程序不提供自定义版本,所以代表此表单使用的字段类型的四个类直接从WTForms
包中导入。 对于每个字段,将在LoginForm
类中将一个对象创建为类变量。 每个字段都有一个描述或标签作为第一个参数。
你在某些字段中看到的可选的验证器(validators
)参数用于将验证行为附加到字段。 DataRequired
验证器仅检查该字段是否未提交为空。 有更多可用的验证器,其中一些将以其他形式使用。
下一步是将表单添加到HTML模板中,以便可以将其呈现在网页上。 好消息是,LoginForm
类中定义的字段知道如何将自己呈现为HTML,因此此任务非常简单。 在下面,你可以看到登录模板,我将其存储在文件app/templates/login.html
中:
<!--app/templates/login.html: Login form template-->
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
对于此模板,我将通过扩展(extends
)模板继承语句再次使用base.html
模板,如第2章所示。实际上,我将使用所有模板来执行此操作,以确保布局一致,其中包括应用程序所有页面上的顶部导航栏。
该模板期望将从LoginForm
类实例化的表单对象作为参数给出,你可以将其视为表单(form
)。这个参数将由登录视图函数发送,我仍然没有写过。
HTML <form>
元素用作Web表单的容器。表单的action
属性用于告知浏览器提交用户在表单中输入的信息时应使用的URL。当操作设置为空字符串时,表单将提交到地址栏中当前存在的URL,即在页面上呈现表单的URL。 method
属性指定将表单提交到服务器时应使用的HTTP请求方法。默认设置是与GET
请求一起发送,但是在几乎所有情况下,使用POST
请求都可以带来更好的用户体验,因为这种类型的请求可以在请求的正文中提交表单数据,而GET
请求可以添加表单URL的字段,使浏览器地址栏混乱。 novalidate
属性用于告诉Web浏览器不要将验证应用于此表单中的字段,从而有效地将此任务留给服务器中运行的Flask应用程序。使用novalidate
完全是可选的,但是对于第一种形式,设置它很重要,因为这将允许你在本章后面测试服务器端验证。
form.hidden_tag()
模板参数会生成一个隐藏字段,其中包含一个令牌,该令牌用于保护表单免受CSRF攻击。要保护表单,你需要做的就是包括此隐藏字段,并在Flask配置中定义SECRET_KEY
变量。如果你做好这两件事,Flask-WTF会为你完成其余的工作。
如果你以前编写过HTML Web表单,则可能发现该模板中没有HTML字段很奇怪。这是因为来自表单对象的字段知道如何将其呈现为HTML。我要做的就是在需要字段标签的地方添加{{ form.<field_name>.label }}
,并在需要字段的地方添加{{ form.<field_name>() }}
。对于需要其他HTML属性的字段,可以将其作为参数传递。此模板中的用户名和密码字段将size
作为参数,并将作为属性添加到<input>
HTML元素。这也是你还可以将CSS类或ID附加到表单字段的方法。
你可以在浏览器中看到此表单的最后一步是在应用程序中编写一个新的视图功能,以呈现上一部分中的模板。
因此,让我们编写一个映射到 /login
URL的新视图函数,该函数创建一个表单,并将其传递给模板进行渲染。 该视图功能还可以与上一个一起进入app/routes.py
模块:
#app/routes.py: Login view function
from flask import render_template
from app import app
from app.forms import LoginForm
# ...
@app.route('/login')
def login():
form = LoginForm()
return render_template('login.html', title='Sign In', form=form)
我在这里所做的是从forms.py
导入LoginForm
类,从中实例化一个对象,然后将其发送到模板。 form = form
语法可能看起来很奇怪,但是只是将在上面一行(在右侧显示)中创建的form
对象传递给名称为form
(在左侧显示)的模板。 这就是呈现表单字段所需要的全部。
为了便于访问登录表单,基本模板可以在导航栏中包含指向它的链接:
<!--app/templates/base.html: Login link in navigation bar-->
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
此时,你可以运行该应用程序并在Web浏览器中查看该表单。 运行该应用程序后,在浏览器的地址栏中键入http://localhost:5000/
,然后单击顶部导航栏中的“Login”链接以查看新的登录表单。 很酷吧?
如果尝试按“提交(submit)”按钮,浏览器将显示“不允许使用的方法”(Method Not Allowed)错误。 这是因为到目前为止,上一节的登录视图功能完成了一半的工作。 它可以在网页上显示表单,但是尚无逻辑来处理用户提交的数据。 这是Flask-WTF使工作变得非常轻松的另一个领域。 这是view函数的更新版本,该函数接受并验证用户提交的数据:
#app/routes.py: Receiving login credentials
from flask import render_template, flash, redirect
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
flash('Login requested for user {}, remember_me={}'.format(
form.username.data, form.remember_me.data))
return redirect('/index')
return render_template('login.html', title='Sign In', form=form)
此版本中的第一个新功能是路由装饰器中的methods
参数。这告诉Flask该视图函数接受GET
和POST
请求,覆盖默认值,即仅接受GET
请求。 HTTP协议指出GET
请求是将信息返回给客户端(在这种情况下为Web浏览器)的请求。到目前为止,应用程序中的所有请求都是这种类型的。当浏览器向服务器提交表单数据时,通常会使用POST
请求(实际上,GET
请求也可以用于此目的,但不建议这样做)。浏览器之前向你显示的“方法不允许”错误出现,因为浏览器尝试发送POST
请求,并且应用程序未配置为接受该请求。通过提供methods
参数,你可以告诉Flask应该接受哪些请求方法。
form.validate_on_submit()
方法完成所有表单处理工作。当浏览器发送GET
请求以表单接收网页时,此方法将返回False
,因此在这种情况下,该函数将跳过if
语句,并直接在函数的最后一行中呈现模板。
当浏览器由于用户按下“提交”按钮而发送POST
请求时,form.validate_on_submit()
将收集所有数据,运行所有附加到字段的验证器,如果一切正常,它将返回True
,指示数据有效,并且可以由应用程序处理。但是,如果至少一个字段未通过验证,则该函数将返回False
,这将导致将该表单呈现给用户,就像在GET
请求的情况下一样。稍后,当验证失败时,我将添加一条错误消息。
当form.validate_on_submit()
返回True
时,登录视图函数调用从Flask导入的两个新函数。 flash()
函数是一种向用户显示消息的有用方法。许多应用程序使用此技术来让用户知道某些操作是否成功。在这种情况下,我将使用此机制作为临时解决方案,因为我还没有真正登录用户所需的所有基础结构。我现在能做的最好的就是显示一条消息,确认该应用程序已收到凭据。
登录视图函数中使用的第二个新函数是redirect()
。此函数指示客户端Web浏览器自动导航到作为参数指定的其他页面。此视图功能使用它将用户重定向到应用程序的索引页。
当你调用flash()
函数时,Flask会存储该消息,但是闪烁的消息不会神奇地出现在网页中。应用程序的模板需要以适用于网站布局的方式呈现这些闪烁的消息。我将这些消息添加到基本模板中,以便所有模板都继承此功能。这是更新的基本模板:
<!--app/templates/base.html: Flashed messages in base template-->
<html>
<head>
{% if title %}
<title>{{ title }} - microblog</title>
{% else %}
<title>microblog</title>
{% endif %}
</head>
<body>
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
<hr>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</body>
</html>
在这里,我使用with
结构将调用get_flashed_messages()
的结果分配给messages
变量,所有这些都在模板的上下文中进行。 get_flashed_messages()
函数来自Flask,并返回以前已使用flash()
注册的所有消息的列表。接下来的条件检查消息是否包含某些内容,在这种情况下,将呈现<ul>
元素,并将每条消息作为<li>
列表项。这种渲染样式看起来并不好,但是稍后将介绍Web应用程序样式的主题。
这些已刷新消息的一个有趣的特性是,一旦通过get_flashed_messages
函数对其进行了一次请求,便将其从消息列表中删除,因此它们仅在调用flash()
函数之后出现一次。
这是一次再次尝试应用程序并测试表单工作方式的好时机。确保尝试在用户名或密码字段为空的情况下提交表单,以查看DataRequired
验证程序如何停止提交过程。
表单字段附带的验证器可防止无效数据被接受到应用程序中。 应用程序处理无效表单输入的方式是通过重新显示表单,以使用户进行必要的更正。
如果你尝试提交无效的数据,我相信你会注意到,尽管验证机制运行良好,但没有向用户指示表单存在问题,用户只是将表单取回。 下一个任务是通过在验证失败的每个字段旁边添加有意义的错误消息来改善用户体验。
实际上,表单验证器已经生成了这些描述性错误消息,因此缺少的只是模板中的一些附加逻辑来呈现它们。
这是在用户名和密码字段中添加了字段验证消息的登录模板:
<!--app/templates/login.html: Validation errors in login form template-->
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
我所做的唯一更改是在用户名和密码字段之后立即添加for
循环,这些字段将验证程序添加的错误消息呈现为红色。 通常,任何附加有验证器的字段都将在 form.<field_name>.errors
格式下添加错误消息。 这将是一个列表,因为字段可以附加多个验证器,并且可能有多个提供错误信息以显示给用户。
如果你尝试使用空的用户名或密码提交表单,现在你将收到一条漂亮的红色错误消息。
现在登录表单已经相当完整,但是在结束本章之前,我想讨论在模板和重定向中包含链接的正确方法。 到目前为止,你已经看到了一些定义链接的实例。 例如,这是基本模板中的当前导航栏:
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
登录视图函数还定义了传递给redirect()
函数的链接:
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# ...
return redirect('/index')
# ...
直接在模板和源文件中编写链接的一个问题是,如果有一天你决定重新组织链接,那么你将不得不在整个应用程序中搜索并替换这些链接。
为了更好地控制这些链接,Flask提供了一个名为url_for()
的函数,该函数使用其内部URL映射来生成URL以查看函数。例如,url_for('login')
返回/login
,而url_for('index')
返回/index
。 url_for()
的参数是端点名称,即视图函数的名称。
你可能会问,为什么最好使用函数名称而不是URL。事实是,与查看功能名称完全是内部的相比,URL更改的可能性更大。第二个原因是,如你稍后将学习的那样,某些URL中包含动态组件,因此,手动生成这些URL将需要连接多个元素,这很繁琐且容易出错。 url_for()
也能够生成这些复杂的URL。
因此,从现在开始,每次需要生成应用程序URL时,我都会使用url_for()
。然后,基本模板中的导航栏变为:
app/templates/base.html: Use url\_for() function for links
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('login') }}">Login</a>
</div>
这是更新后的login()
视图函数:
#app/routes.py: Use url_for() function for links
from flask import render_template, flash, redirect, url_for
# ...
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# ...
return redirect(url_for('index'))
# ...
(本篇结束)