处理表单

表单是让用户与我们的网页应用程序交互的基本元素。Flask 本身并不会帮助我们处理表单,但是 Flask-WTF 扩展让我们在我们的 Flask 应用程序中使用流行的 WTForms 包。这个包使得定义表单和处理提交容易一些。

Flask-WTF

我们想要使用 Flask-WTF 做的第一件事情(在安装它以后)就是在 myapp.forms 包中定义一个表单。

# ourapp/forms.py

from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email

class EmailPasswordForm(Form):
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
  • 在 Flask-WTF 0.9 版本以前,Flask-WTF 提供了针对 WTForms 字段以及验证器的自己的封装。你可能看到外面一大堆的代码是从 flask.ext.wtforms 中不是从 wtforms 中导入 TextFieldPasswordField
  • 在 Flask-WTF 0.9 版本以后,我们应该直接从 wtforms 中导入这些字段和验证器。

我们定义的表单是一个用户登录表单。我们把它叫做 EmailPasswordForm(),我们可以重用这个同样的表单类(Form)去做其它的一些事情,像注册表单。这里我们没有去定义一个又长又没有用的表单,而是选择一个很常用的表单,只是为了给你们介绍使用 Flask-WTF 定义表单的方式。也许以后在正式项目中会定义一个特别复杂表单。对于表单中包含字段名称,我们建议使用一个清楚的名称,并且在一个表单中保持唯一。不得不说,对于一个长的表单,我们可能要给出一个更符合上文的字段名称。

登录表单可以替我们做一些事情。它能够保证我们应用程序的安全以防止 CSRF 漏洞,验证用户输入并且渲染适当的标记,这些标记是我们为表单定义的字段。

CSRF 保护和验证

CSRF 表示跨站请求伪造。CSRF 攻击是指第三方伪造(像一个表单提交)请求到一个应用程序的服务器。一个易受攻击的服务器假设从一个表单来的数据是来自它自己的网站并且采取相应的操作。

作为一个例子,比方说,一个邮件提供商可以让你通过提交一个表单来删除你的账号。表单发送一个 POST 请求到服务器上的 account_delete 端点并且当表单被提交的时候删除登录的账号。我们可以在自己的网站上创建一个表单,该表单发送一个 POST 请求到同一个 account_delete 端点。现在,如果我们让某人点击我们表单的提交按钮(或者通过 JavaScript 来这样做),邮件提供商提供的登录账号就会被删除掉。当然邮件提供商还不知道表单提交并不是发生在他们的网站上。

因此如何才能阻止 POST 请求来自别的网站?WTForms 通过在渲染每一个表单的时候生成一个唯一的令牌使得成为可能。生成的令牌会被传回到服务器,伴随着 POST 请求的数据,在表单被接受之前令牌必须接受服务器的验证。关键的是令牌是与存储在用户会话(cookies)的一个值有关并且会在一段时间后失效(默认是 30 分钟)。这种方式就能够保证提交一个有效表单的人就是加载页面的人(或者至少是使用同一电脑的人),而且他们只能在加载页面 30 分钟内这样做。

要开始使用 Flask-WTF 保护 CSRF,我们需要为我们的登录页定义一个视图。

# ourapp/views.py

from flask import render_template, redirect, url_for

from . import app
from .forms import EmailPasswordForm

@app.route('/login', methods=["GET", "POST"])
def login():
    form = EmailPasswordForm()
    if form.validate_on_submit():

        # Check the password and log the user in
        # [...]

        return redirect(url_for('index'))
    return render_template('login.html', form=form)

如果表单已经被提交和验证的话,我们可以继续登录的逻辑。如果它没有被提交的话(例如,只是一个 GET 请求),我们就要把表单对象传递给我们的模板,以便它能够被渲染。下面就是我们使用 CSRF 保护的时候模板的样子。

{# ourapp/templates/login.html #}

{% extends "layout.html" %}
{% endraw %}
<html>
    <head>
        <title>Login Page</title>
    </head>
    <body>
        <form action="{{ url_for('login') }}" method="post">
            <input type="text" name="email" />
            <input type="password" name="password" />
            {{ form.csrf_token }}
        </form>
    </body>
</html>

{% raw %}{{ form.csrf_token }}{% endraw %} 渲染了一个隐藏的字段,该字段包含那些奇特的 CSRF 令牌,并且当 WTForms 验证表单的时候会寻找这个字段。我们不用担心包含处理令牌的逻辑,WTForms 会主动帮我们去做。好哇!

使用 CSRF 令牌保护 AJAX 调用

Flask-WTF CSRF 令牌不限于保护表单提交。如果你的应用程序要处理其它可能会被伪造的请求(特别是 AJAX 调用),你也可以在那里添加 CSRF 保护!

自定义验证

除了由 WTForms 提供的内置的表单验证器(例如,Required()Email() 等等),我们能创建我们自己的验证器。我们将通过编写一个 Unique() 验证器来说明如何创建自己的验证器,Unique() 验证器是用来检查数据库并且确保用户提供的值在数据库中不存在。这能够用于确保用户名或者邮箱地址还没有使用。没有 WTForms 的话,我们可能要在视图中做这些事情,但是现在我们可以在表单本身做些事情。

现在我们来定义一个简单的注册表单,其实这个表单和登录的表单几乎一样。只是会在后面给它添加一些自定义的验证器。

# ourapp/forms.py

from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email

class EmailPasswordForm(Form):
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])

现在我们要添加我们的验证器用来确保它们提供的邮箱地址不存在数据库中。我们把这个验证器放在一个新的 util 模块,util.validators

# ourapp/util/validators.py
from wtforms.validators import ValidationError

class Unique(object):
    def __init__(self, model, field, message=u'This element already exists.'):
        self.model = model
        self.field = field

    def __call__(self, form, field):
        check = self.model.query.filter(self.field == field.data).first()
        if check:
            raise ValidationError(self.message)

这个验证器假设我们是使用 SQLAlchemy 来定义我们的模型。WTForms 期待验证器返回某种可调用的对象(例如,一个可调用的类)。

Unique()\\_\\_init\\_\\_ 中我们可以指定哪些参数传入到验证器中,在本例中我们要传入相关的模型(例如,在我们例子中是传入 User 模型)以及要检查的字段。当验证器被调用的时候,如果定义模型的任何实例匹配表单中提交的值,它将会抛出一个 ValidationError。我们也可以添加一个具有通用默认值的消息,它将会被包含在 ValidationError 中。

现在我们可以修改 EmailPasswordForm,使用我们自定义的 Unique 验证器。

# ourapp/forms.py

from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired

from .util.validators import Unique
from .models import User

class EmailPasswordForm(Form):
    email = StringField('Email', validators=[DataRequired(), Email(),
        Unique(
            User,
            User.email,
            message='There is already an account with that email.')])
    password = PasswordField('Password', validators=[DataRequired()])
  • 我们的验证器不必须是一个可调用的类。它也可能是返回可调用或者直接调用的一个工厂模式。WTForms 文档中有 一些例子

渲染表单

WTForms 也能帮助我们为表单渲染成 HTML 表示。WTForms 实现的 Field 字段能够渲染成该字段的 HTML 表示,所以为了渲染它们,我们只必须在我们模板中调用表单的字段。这就像渲染 csrf_token 字段。下面给出了一个登录模板的示例,在里面我们使用 WTForms 来渲染我们的字段。

{# ourapp/templates/login.html #}

{% extends "layout.html" %}
<html>
    <head>
        <title>Login Page</title>
    </head>
    <body>
        <form action="" method="post">
            {{ form.email }}
            {{ form.password }}
            {{ form.csrf_token }}
        </form>
    </body>
</html>

我们可以自定义如何渲染字段,通过传入字段的属性作为参数到调用中。

<form action="" method="post">
    {{ form.email.label }}: {{ form.email(placeholder='yourname@email.com') }}
    <br>
    {% raw %}{{ form.password.label }}: {{ form.password }}{% endraw %}
    <br>
    {% raw %}{{ form.csrf_token }}{% endraw %}
</form>
  • 如果我们想要传入 “class” HTML 属性,我们必须使用 class_='' 因为 “class” 是 Python 中的保留关键字。
  • WTForms 文档中有一个 可用字段属性列表
  • 你可能注意到我们没有必须要使用 Jinja 的 |safe 过滤器。这是因为 WTForms 渲染 HTML 安全字符串。
  • 更多的内容请参阅 官方文档

摘要

  • 表单从安全性的角度来看是很可怕的。
  • WTForms(以及 Flask-WTF)使得容易地定义,保护以及渲染你的表单。
  • 使用 Flask-WTF 提供的 CSRF 保护可以确保你的表单的安全。
  • 你也可以使用 Flask-WTF 来保护你的 AJAX 调用以防止 CSRF 攻击。
  • 定义自定义的表单验证器可以让验证逻辑远离视图。
  • 使用 WTForms 字段渲染来渲染你的表单的 HTML,在你对你的表单定义做出一些改变的时候,你不必每次都更新它。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容