十四、表单

157 阅读10分钟

表单是一个网站与用户交互必不可少的元素,表单中可以提供文本输入框、单选按钮、复选框、按钮等元素供用户提交数据,在flask项目中,表单除了可以表示传统的HTML标签外,还有验证数据的作用。数据被发送到服务器后,服务器为了防止不法分子绕过前端限制提交一些非法数据,需要对提交上来的数据进行验证,验证合法后才进行后续的操作,要实现表单的验证功能,我们需要借助第三方插件Flask-WTF,Flask-WTF是对ETForms库的封装,让WTForms库在Flask项目中更方便地被使用,不过Flask-WTF提供的功能比较有限,大部分功能是直接从WTForms中直接导入的,WTForms的功能主要有两个,分别是验证数据和在模版中渲染表单HTML标签,当然,WTForms还包括一些其他功能,如CSRF保护、文件上传等,安装Flask-WTF的同时默认也会安装WTForms,安装命令如下:

pip3 install flask-wtf

安装完Flask-WTF后,任意创建一个项目,开始编写代码

6.1、表单验证

这里以注册为例,讲解表单验证功能,注册时需要提交邮箱、用户名、密码、确认密码4个字段的数据,首先在templates文件夹中创建一个register.html文件,然后输入以下代码:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>注册</title>  
</head>  
<body>  
    <form action="{{url_for('register')}}" method="post">  
        <table>  
            <tr>  
                <td> 用户名: </td>  
                <td><input type="text" name="username"></td>  
            </tr>  
            <tr>  
                <td>邮箱:</td>  
                <td><input type="email" name="email"></td>  
            </tr>  
            <tr>  
                <td>密码:</td>  
                <td><input type="password" name="password"></td>  
            </tr>  
            <tr>  
                <td>确认密码:</td>  
                <td><input type="password" name="confirm_password"></td>  
            </tr>  
            <tr>  
                <td></td>  
                <td><input type="submit" value="提交"></td>  
            </tr>  
        </table>    
    </form>  
</body>  
</html>

以上代码中,首先创建了一个form标签,然后设置action为url_for('register'),也就是将register视图函数反转为URL,在以后单击‘提交’按钮时,会把所有form标签下输入框中的内容都提交给这个URL(看下文register视图函数)。接着还设置了method为POST,这意味着会以POST方式提交。然后在form标签下,分别添加了属性name为username、email、password以及confirm_password的input标签,最后添加了一个type=“submit”的input标签,被渲染出来是一个按钮。

模版写好后,再用一个视图函数渲染,示例代码如下:

#falsk-sqlalchemy的基本使用  
  
from flask import Flask, jsonify, render_template,request,redirect,url_for,flash   
from flask import get_flashed_messages  
  
app = Flask(__name__)   
app.secret_key = "fhowehfohe"   #设置秘钥
   
   
 #关键代码
@app.route('/register',methods=["GET",'POST'])  
def register():  
    if request.method == 'GET':  
        return render_template('register.html')  
    else:  
         pass
  
if __name__ == '__main__':  
    app.run(host="111.222.33.444",port=8080,debug=True)

执行以上代码后,再浏览器中访问:http://111.222.33.444:8080/register,即可看到如下图所示的效果

image.png

6.1.1 表单类编写

目前为止,我们完成了前端模版代码的编写,用户可以在此页面输入信息进行注册了。但是在此页面中,对每个字段是有一定要求的,要求如下:

  • 用户名:为了防止重名,一般要求最少要输入3位以上字符
  • 邮箱:格式必须以@+域名结尾
  • 密码:要求最少输入6位以上字符
  • 确认密码:内容必须和密码字段内容一致

我们不能要求用户一次性就正确输入满足这些规则的内容,如果用户输入错误,应该在界面中及时给予提示,这个工作可以由前端通过JavaSicrpt来完成,但是服务器端也要做好验证,因为对于有一定技术功底的用户来说,可以通过抓包的形式获取注册时的请求数据,然后通过代码或者工具来模拟注册,这就完全绕开了前端的JavaScript验证。服务器端的验证可以通过WTForms来实现。首先在项目根路径下创建一个forms.py文件,然后写入一下代码:

from wtforms import Form,StringField  
from wtforms.validators import length,email,equal_to  
  
class RegisterForm(Form):  
username = StringField(validators=[length(min=3,max=20,message="请输入正确长度的用户名!")])  
email = StringField(validators=[email(message="请输入正确格式的邮箱!")])  
password = StringField(validators=[length(min=6,max=20,message="请输入正确长度的密码!")])  
confirm_password = StringField(validators=[equal_to("password",message="两次密码输入不一致!")])

上述代码中,先从wtforms中导入From基类,所有的表单类都必须继承自Form基类,然后在RegisterForm中分别添加了username、email、password、confirm_password这4个字段,这里字段的名称必须和HTML模版中表单元素的name的值一致,如在HTML模版中邮箱的input标签的name值为email,那么在RegisterForm中字段的名称也必须为email。

这4个属性现在都是字符串类型,因此使用StringField类型,除StringField外,还有一下类型的Field类,如表

字段类型描述
StringField字符串类型
IntegerField整形类型
FloatField浮点类型
DecimalField定点类型
BooleanField布尔类型
DateTimeField日期时间类型
DateField日期类型
TimeField时间类型
FileField文件类型

每个字段都传递了validators参数,这个参数数可以存储多个验证器的集合,不同的字段应根据实际需要设置不同的验证器。

  • username:添加了length验证器,用来规定最短字符串长度为3,最长字符串长度为20,并且如果上传的值不在这个范围内,会提示一个错误信息,错误信息的内容就是message指定的值。
  • email:添加了email验证器,email验证器会自动验证上传的值是否满足邮箱的格式规则,如果不满足,同样会提示message指定的错误信息
  • password:同样用的是length验证器,指定字符长度为6-20
  • confirm_password:确认密码用的是equal_to验证器,WTForms还提供了一下验证器,如表
验证器描述
length(min,max,message)验证长度是否在区间内
email()验证内容是否满足邮箱格式规则
equal_to(fieldname,message)验证是否和另外一个字段的值相等
ip_address(ipv4,ipv6,message)验证是否满足IP地址的规则
mac_adress(meaasge)验证是否满足mac地址的规则
number_range(min,max,message)验证数字是否在指定的区间内
optional(strip_whitespace)设置数据可以为空,并停止其他验证器的验证
input_required(message)验证是否为空
data_required(message)验证是否有效
url(message)验证是否满足URL规则
any_of(values,message,values_formatter)验证是否是values中的一个
none_of(values,message,values_formatter)验证是否不是values中的一个
regexp(regex,flags,message)自己指定正则表达式验证

6.1.2 视图函数中使用表单

表单写完后,就可以在视图函数中对数据进行校验了,这里以继续完善register视图函数为例,来讲解表单在视图函数中的使用。

from flask import Flask, jsonify, render_template,request,redirect,url_for,flash   
from flask import get_flashed_messages  
  
app = Flask(__name__)   
app.secret_key = "fhowehfohe"   #设置秘钥
   
   
 #关键代码
@app.route('/register',methods=["GET",'POST'])  
def register():  
    if request.method == 'GET':  
        return render_template('register.html')  
    else:  
         #request.form 是html模版提交上来的表单数据  
        form = RegisterForm(request.form)  
        #如果表单验证通过  
        if form.validate():  
            email = form.email.data  
            username = form.username.data  
            password = form.password.data  
            #以下是可以把数据保存到数据库的操作  
            print("email:",email)  
            print("username:",username)  
            print("password:",password)  
            return "注册成功!"  
        else:  
            for errors in form.errors.values():  
                for error in errors:  
                    flash(error)  
            return redirect(url_for('register'))
  
if __name__ == '__main__':  
    app.run(host="111.222.33.444",port=8080,debug=True)

以上代码中,先使用RegisterForm创建了一个form对象,并且把request.form作为参数传给RegisterForm,request.form是一个类字典类型,以键-值对的形式保存了从浏览器中提交上来的表单数据,然后再调用form.validate()方法判断RegisterForm中定义的所有字段是否都验证通过,如果是,则通过form.<字段名>.data来获取对应字段的数据,这里是从form对象上获取数据而不是从request.form上获取数据的原因是,服务器获取从浏览器提交上拉了的数据,其本质上都是字符串类型,所以request.form上所有数据都是字符串类型,但是通过form对象获取的数据则是经过处理后的,如某个字段是IntegerField,那么通过form对象获取到的则是整数,以上代码有个小细节,在视图中不需要获取confirm_password的值,原因是confirm_password字段存在的意思就是为了验证其值和passwrod是否一致,如果表单验证通过,则意味着confirm_password和password是相等的,所以没有必要再重新获取一次了,在所有数据都获取到后,就可以把数据存储到数据库中,或者再做其他操作。

如果表单验证失败,则可以通过form.errors获取错误信息,form.errors获取错误信息。form.errors是字典类型,key是字段名称,value是错误信息的列表,如在注册表单中什么都不输入,直接单击‘提交’按钮,则form.errors的值如下所示。

-   请输入正确长度的用户名!
-   请输入正确格式的邮箱!
-   请输入正确长度的密码!

所以在表单验证失败的情况下,首先通过循环form.errors.values()获取所有错误内容,并存储到flash中,然后在模版中把flash消息显示出来,register.html修改后的代码如下:

...
        <ul>  
            {% for message in get_flashed_messages() %}  
                <li>{{ message }}</li>  
            {% endfor %}  
        </ul>  
    </form>  
</body>  
</html>

注意:使用flash消息,必须先在app上配置SECRET_KEY,或直接通过app.secret_key来设置秘钥

如果再浏览器中访问http://xxx:8080/register,然后在表单中不输入任何信息,直接单击‘提交’按钮,那么网页将展现出如图所示的效果

image.png

6.1.3 自定义验证字段

虽然WTForms中提供了许多验证器,但有时候我们还是需要自定义验证逻辑,还是已RegisterForm为例,在验证email字段时,除了验证是否满足邮箱的格式规则,还需要验证邮箱是否已被注册过,这时就必须查询数据库,判断邮箱是否存在,如果要自定义某个字段的额验证逻辑,可以通过在表单类中自定义方法validate_<字段名>来实现,这里以验证email为例,示例代码如下。

from wtforms import Form,StringField,ValidationError  
from wtforms.validators import length,email,equal_to  
  
  
registed_email = ["aa@163.com","bb@163.com"]  
  
class RegisterForm(Form):  
    username = StringField(validators=[length(min=3,max=20,message="请输入正确长度的用户名!")])  
    email = StringField(validators=[email(message="请输入正确格式的邮箱!")])  
    password = StringField(validators=[length(min=6,max=20,message="请输入正确格式的密码!")])  
    confirm_password = StringField(validators=[equal_to("password",message="请保持和设置密码一致!")])  

    def validate_email(self,field):  
        email = field.data  
        if email in registed_email:  
            raise ValidationError("邮箱已注册!")  
        else:  
            True

这里为了模拟从数据库中判断邮箱是否已被注册,定义了一个registed_email变量,代表已经被注册的邮箱,也可结合ORM知识实现真实的数据库查找,接着定义了一个validate_email(self,field)方法,以后在视图函数中调用form.validate()方法时,RegisterForm底层会自动调用validate_email方法,并且会传递一个field参数,这里因为验证的是email字段,所以这个field参数代表的是email字段,如果验证的是其他字段,则field会代表相应字段,然后通过field.data拿到对应的值,再进行逻辑判断,如果验证失败了,则可抛出wtforms.ValidationError异常,并且指定一个错误信息,这个错误信息回出现在form.errors中,否则直接返回True即可

6.2、渲染表单模版

6.3、CSRF攻击