表单是一个网站与用户交互必不可少的元素,表单中可以提供文本输入框、单选按钮、复选框、按钮等元素供用户提交数据,在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,即可看到如下图所示的效果
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,然后在表单中不输入任何信息,直接单击‘提交’按钮,那么网页将展现出如图所示的效果
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攻击