Flask 的表单验证器

在 Web 页面中,表单是一种常见的元素,表单包含有多个字段,通常字段的取值需要在一定的范围内,例如:QQ 注册时,名称不可以为空,密码的长度至少是 8 个字符,如下图所示:

将表单提交给服务端处理时,服务端需要验证表单中的字段的取值是否符合要求。本节学习 Flask 中提供表单验证的功能,学习如何对表单中的字段的取值进行有效性检查。

1. 表单验证器

1.1 WTForms 和 Flask-WTF

在 Python 的 Web 开发中,WTForms 是一个灵活的表单验证和表单渲染的库,它的主要功能:

  • 验证表单中的字段的取值是否符合要求;
  • 渲染输出表单的 HTML 代码。

WTForms 可以与任意的 Web 框架和模板引擎一起使用。 在 Flask 框架或者 Django 框架中,都可以使用 WTForms。

Flask-WTF 在 WTForms 的基础上提供了一些扩展,可以方便的在 Flask 框架中生成表单。

1.2 表单字段

WTForms 支持如下类型的表单字段:

字段类型 说明
StringField 文本字段
TextAreaField 多行文本字段
PasswordField 密码文本字段
HiddenField 隐藏文本字段
DateField 文本字段,值为datetime.date格式
IntegerField 文本字段,值为整数
DecimalField 文本字段, 值为decimal.Decimal
FloatField 文本字段,值为浮点数
BooleanField 复选框, 值为True 和 False
RadioField 一组单选框
SelectField 下拉列表
FileField 文件上传字段
SubmitField 表单提交按钮

1.3 验证器

WTForms 支持如下类型的表单验证:

验证类型 说明
Email 验证电子邮件地址
EqualTo 比较两个字段的值;常用于要求输入两次秘钥进行确认的情况
Length 验证输入字符串的长度
NumberRange 验证输入的值在数字范围内
DateRequired 确保字段中有数据

2. 程序功能和结构

2.1 程序功能

在下面的小节,我们将使用 WTForms 实现一个登录程序,表单中包含有如下字段:

表单字段 说明
邮件地址 要求符合邮件地址的格式,如 user@qq.com
密码 要求密码至少包括 6 个字符
提交 将表单提交给服务器

用户通过浏览器进行登录,界面如下所示:

图片描述

用户输入邮件地址为 ‘tom’、密码为 ‘123’,点击登录按钮将表单提交给服务器,服务器进行表单验证:

  • 验证邮件地址是否规范,并显示验证失败的信息——“请输入正确的邮箱” ;
  • 验证密码的长度,并显示验证失败的信息——“密码至少包括 6 个字符”。

2.2 程序结构

例子包括 2 个源文件,如下表所示:

程序 说明
app.py Flask 后端程序,实现表单验证
templates/login.html 登录界面的模板

2.3 源程序下载

3. Flask 程序 app.py

3.1 安装库

使用如下命令安装相关的库:

$ pip3 install wtforms
$ pip3 install flask-wtf
$ pip3 install email-validator

3.2 引入库

#!/usr/bin/python3
from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField
from wtforms.validators import DataRequired, Length, Email, ValidationError

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

在这个程序中,引入如下类:

来自于模块 功能
FlaskForm flask_wtf 登录表单的基类
StringField wtforms 文本字段
PasswordField wtforms 密码文本字段
SubmitField wtforms 表单提交按钮
DateRequired wtforms 确保字段中有数据
Length wtforms 验证输入字符串的长度
Email wtforms 验证电子邮件地址的格式是否正确
ValidationError wtforms 验证失败时,抛出此异常

在第 8 行,配置 Flask 应用的选项 ‘SECRET_KEY’ 用于防范 CSRF 攻击,请参考 CSRF 攻击的相关词条。

3.3 定义登录表单

class LoginForm(FlaskForm):
    email = StringField(
        label = '邮箱',
        validators = [
            DataRequired(message = '邮箱不能为空'),
            Email(message = '请输入正确的邮箱')
        ]
    )

    password = PasswordField(
        label = '密码',
        validators =[
            DataRequired(message = '密码不能为空'),
            Length(min = 6, message = '密码至少包括 6 个字符')
        ]
    )

    submit = SubmitField('登录')

定义类 LoginForm,它继承于 FlaskForm,用于描述登录界面,登录界面是一个表单,包含有 3 个字段:

  • email,显示 label 为 ‘邮箱’,包括 2 个验证器:DataRequired 和 Email,message 参数为验证失败的提示信息;
  • password,显示 label 为 ‘密码’,包括 2 个验证器:DataRequired 和 Length,message 参数为验证失败的提示信息,min = 6 表示密码的最小长度;
  • submit,提交按钮,提交表单给服务端。

3.4 进行表单验证

@app.route('/', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    print('form.validate_on_submit() =', form.validate_on_submit())
    print('form.email.label =', form.email.label)
    print('form.email() = ', form.email)
    print('form.email.errors =', form.email.errors)
    return render_template('login.html', form=form)

app.run(debug=True)

在第 1 行,设置以 GET 方法或者 POST 方法访问路径 / 时,使用函数 login() 进行处理;在第 3 行,创建一个实例 form,表示用户登录的表单;在第 8 行,调用 render_template 渲染 login.html。

form 对象提供了如下方法和属性:

属性 说明
form.validate_on_submit() 表单验证函数,返回值表示验证是否正确
form.email() 显示 email 字段对应的 HTML 代码
form.email.label email 字段的 label
form.email.errors 验证 email 字段的失败提示信息

在程序中打印 form 的属性,当用户提交表单时,在控制台中显示如下信息:

form.validate_on_submit() = False
form.email.label = <label for="email">邮箱</label>
form.email() =  <input id="email" name="email" required type="text" value="tom">
form.email.errors = ['请输入正确的邮箱']

当表单验证失败时,form.validate_on_submit() 返回为 False。form.email.errors 是一个列表,记录了所有可能的错误信息。

4. 模板文件 login.html

<html>
<meta charset='UTF-8'>
<h1>登录</h1>
<form class="form" method="POST">
    <div>
        {{ form.email.label }}
        {{ form.email() }}
        <b>{{ form.email.errors[0] }}</b>
    </div>

    <div>
        {{ form.password.label }}
        {{ form.password() }}
        <b>{{ form.password.errors[0] }}</b>
    </div>

    <div>
        {{ form.submit() }}
    </div>

    {{ form.hidden_tag() }}
</form>
</html>

login.html 是用于描述登录界面的模板,根据 form 中字段 email 和 password 的属性,它被渲染为如下的 HTML 文件:

<html>
<meta charset='UTF-8'>
<title>登录</title>
<h1>登录</h1>
<form class="form" method="POST">
    <div>
        <label for="email">邮箱</label> 
        <input id="email" name="email" required type="text" value="tom"> 
        <b>请输入正确的邮箱</b>
    </div>

    <div>
        <label for="password">密码</label> 
        <input id="password" name="password" required type="password" value=""> 
        <b>密码至少包括 6 个字符</b>
    </div>

    <div>
        <input id="submit" name="submit" type="submit" value="登录">
    </div>

    <input id="csrf_token" name="csrf_token" type="hidden" value="ImY2Y2NhMWNjZDRlYWE1ZDE2ODRiZDFlYzY5ZGNhNDIzZWJmNWQ0OTQi.X1Fo_w.13ad614Bw80RgPhc9RFdjZw7-q0">
</form>
</html>

这里注意两点:

  • form.email.errors 和 form.password.errors 是一个错误信息列表,errors[0] 表示第一条错误信息;
  • form.hidden_tag() 用于防范 CSRF 攻击,生成 <input id=“csrf_token”/> 标签,请参考相关词条。

5. 自定义验证器

5.1 简介

在 Flask 中,可以自定义验证器实现特定的验证需求,例如:在验证密码字段时,要求密码不能全部都是数字。

Flask 包含有两种类型的验证器:行内验证器和全局验证器。下面通过具体的例子说明如何实现自定义验证 “密码是否全部是数字”。

5.2 行内验证器

对前面小节的例子程序进行如下的局部修改,修改类 LoginForm,增加一个成员函数 validate_password,如下:

class LoginForm(FlaskForm):
    def validate_password(self, field):
        for char in field.data:
            print('!', char)
            if '0123456789'.find(char) < 0:
                return
        raise ValidationError('密码不能全部是数字')

在第 2 行,定义成员函数 validate_password,验证函数的形式为 validate_字段名,在验证字段数据时会调用这个方法来验证对应的字段,在这里调用函数 validate_password 验证字段 password。

在第 3 行到第 6 行,遍历密码字段 field 的每个字符,如果发现存在一个非数字的字符,则正常返回;如果所有的字符都是数字,则抛出异常 ValidationError。

当用户输入的密码全部都是数字时,表单验证失败,提示错误信息为:‘密码不能全部是数字’。

5.3 全局验证器

如果想要创建一个可重用的通用验证器,可以通过定义一个全局函数来实现。

对前面小节的例子程序进行如下的局部修改,增加一个全局函数 validate_password,如下:

def can_not_be_all_digits(form, field):
    for char in field.data:
        print('!', char)
        if '0123456789'.find(char) < 0:
            return
    raise ValidationError('密码不能全部是数字')

函数 can_not_be_all_digits 验证字段 field 是否全部是数字,代码与上一个小节中的函数 validate_password 完全相同。

同时修改类 LoginForm 的 PasswordField,如下:

class LoginForm(FlaskForm):
    password = PasswordField(
        label = '密码',
        validators =[
            DataRequired(message = '密码不能为空'),
            Length(min = 6, message = '密码至少包括 6 个字符'),
            can_not_be_all_digits
        ]
    )

在第 7 行,validatros 中增加一个新的 validator——can_not_be_all_digits。当用户输入的密码全部都是数字时,表单验证失败,提示错误信息为:‘密码不能全部是数字’。

5.4 源程序下载

6. 小结

本小节讲解了 Flask 的表单验证的概念和使用,总结如下:

图片描述

使用 Flask 的表单验证功能,可以方便的验证表单中的字段的有效性,并渲染输出表单的 HTML 代码。