06.表单WTForms
作用
带有 csrf 令牌的安全表单。
全局的 csrf 保护。
支持验证码(Recaptcha)。
与 Flask-Uploads 一起支持文件上传。
国际化集成。
安装
pip install Flask-WTF
导入
from flask import Flask
from flask_wtf import Form
from wtforms import StringField
from wtforms.validators import DataRequired
创建表单
演示
app.py
from flask import Flask,render_template,request
from flask_wtf import Form
from wtforms import StringField
from wtforms.validators import DataRequired,Length,EqualTo
app = Flask(__name__)
app.config["SECRET_KEY"] = "12345678"
class TestForm(Form):
name = StringField("name",validators=[DataRequired(),Length(min=3,max=10,message="用户名长度必须在3到10位之间")])
password1 = StringField("password1",validators=[Length(min=6,max=10)])
# EqualTo指定与之保持相同值的字段
password2 = StringField("password2",validators=[Length(min=6,max=10),EqualTo("password1")])
@app.route('/login/',methods=["GET","POST"])
def login():
if request.method == "GET":
return render_template("login.html")
else:
form = TestForm(request.form)
if form.validate(): # 如果通过验证,返回为True,否则False
return "{}".format(form.data)
else:
return "{}".format(form.errors) # 打印错误信息
if __name__ == '__main__':
app.run()
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
</head>
<body>
<form>
<input type="text" name="name"> <br>
<input type="text" name="password1"> <br>
<input type="text" name="password2"> <br>
<button id="submit">提交</button>
</form>
<script>
$(function () {
$("#submit").click(function (event) {
{# 阻止默认事件发生#}
event.preventDefault()
var name = $("form>input[name='name']").val()
var password1 = $("input[name='password1']").val()
var password2 = $("input[name='password2']").val()
$.ajax({
type: 'POST',
url: "/login/",
data: {
"name":name,
"password1":password1,
"password2":password2,
},
success: function (data) {
console.log("post 请求成功")
},
});
})
})
</script>
</body>
</html>


问题
添加app.config["SECRET_KEY"] = "xxxx"
即可解决如下问题
RuntimeError: A secret key is required to use CSRF.
表单定义
label:字段的名字
default:默认
validators:字段的验证序列
description:字段的描述
choices:SelectField和他的子类有的字段,选择框,多选一
class UserAdminForm(FlaskForm):
username = StringField(label='用户名', validators=[DataRequired(),Length(4,20)])
password_hash = PasswordField(label='密码',validators=[DataRequired(),Length(4,20)])
limit = SelectField(label='用户权限',
choices=[('guest', '所有权限'),
('operation', '可读可写不可删除'),
('management', '可读不可写')],
default='guest') # 权限
获取数据
由get请求发送过来的数据,通过request.args.get("属性名称")
进行获取,由post请求发送过来的数据,如果传递过来的为文件,通过request.fiels
,如果是数据,通过request.form
。
常用字段
from wtforms import *
StringField
文本框
TextAreaField
多行文本框
PasswordField
密码输入框
HiddenField
隐藏文本框
DateField
接收给定格式datetime.date型的文本框
DateTimeField
接收给定格式datetime.datetime的文本框
IntegerField
接收整型的文本框
DecimalField
接收decimal.Decimal型的文本框
FloadField
接收浮点型的文本框
BooleanField
带有True和False的复选框
RadioField
—组单选框
SelectField
下拉选择框
SelectMultipleField
下拉多选框
FileField
文件上传框
SubmitField
表单提交按钮
FormField
将一个表单作为表单域嵌入到容器表单中
FieldList
给定类型的表单域列表
验证表单
from wtforms.validators import *
验证邮箱地址
DataRequired
验证数据是否真实存在,即不能为空,必须是非空白字符串,否则触发StopValidation错误。
EqualTo
比较两个域的值;在要求输入两次密码进行确认的时候非常有 用
IPAddress
验证|Pv4网络地址
Length
验证输入字符串的长度
NumberRange
验证输入的值在数值范围内
Optional
允许输入为空;忽略额外的验证
Required
验证表单域包含数据
Regexp
验证输入的正则表达式
URL
验证--个URL
AnyOf
验证输入是一组可能值中的--个
NoneOf
验证输入不是一组可能值中的一个
数据发过来时,经过表单验证,因此需要验证器来进行验证,以下对一些常用的内置验证器进行讲解:
Email:验证上传的数据是否为邮箱
# 验证邮箱
email = StringField(validators=[Email()])
EqualTo:验证上传的数据是否和另外一个字段相等,常用的就是密码和确认密码两个字段是否相等
# EqualTo指定与之保持相同值的字段
password_repeat = StringField(validators=[Length(min=6,max=10),EqualTo("password")])
InputRequired:原始数据的需要验证。如果不是特殊情况,应该使用InputRequired。
# 验证用户是否输入
username = StringField(validators=[input_required()])
Length:长度限制,有min和max两个.值进行限制
username = StringField(validators=[Length(min=3,max=10,message="用户名长度必须在3到10位之间")])
NumberRange:数字的区间,有min和max两个值限制,如果处在两个数字之间则满足
# 验证范围
age = IntegerField(validators=[NumberRange(12,18)])
Regexp:自定义正则表达式
# 正则表达式
phone = StringField(validators=[Regexp(r'1[34578]\d{9}')])
URL:必须要是URL的形式
# url验证
home_page = StringField(validators=[URL()])
UUID:验证UUID
# uuid值验证
uuid = StringField(validators=[UUID()])
自定义表单验证器
namefield验证器
class LoginForm(Form):
.....
# 1234
captcha = StringField(validators=[Length(4,4)])
# 自定义验证器
# valiedate_name
def validate_captcha(self,field):
# print(type(field))
# 字段数据值
# print(field.data)
if field.data != "1234":
raise ValidationError("验证码错误") # 抛出错误信息
通过valiedate_name
(其中name为定义的属性),定义一个函数自定义验证属性值是否合法。
把验证器编写成单独的函数
def validate_captcha(form,field):
# print(type(field))
# 字段数据值
# print(field.data)
if field.data != "1234":
raise ValidationError("验证码错误")
class LoginForm(Form):
.....
# 1234
captcha = StringField(validators=[validate_captcha,Length(4,4)])
使用WTForms渲染模版
app.py
from flask import Flask,render_template,request
from flask_wtf import Form
from wtforms import StringField,IntegerField,BooleanField,SelectField
from wtforms.validators import InputRequired,NumberRange
app = Flask(__name__)
app.config["SECRET_KEY"] = "12345678"
class SettingsForm(Form):
username = StringField(label="用户名",validators=[InputRequired()])
age = IntegerField("年龄",validators=[NumberRange(0,100)])
remember = BooleanField("记住")
tags = SelectField("标签",choices=[("1","python"),("2","java"),("3","c++")])
@app.route('/',methods=["GET","POST"])
def settings():
if request.method == "GET":
form = SettingsForm(request.form,csrf_enabled=False)
return render_template("settings.html",form=form)
else:
form = SettingsForm(request.form,csrf_enabled=False)
if form.validate():
return "{}".format(form.data)
else:
return "{}".format(form.errors)
if __name__ == '__main__':
app.run()
settings.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style type="text/css">
.username-input{
background-color: red;
}
</style>
</head>
<body>
<form action="" method="post">
<table>
<tbody>
<tr>
<td>{{ form.username.label }}</td>
{# <td><input type="text" name="username"/></td>#}
<td>{{ form.username(class="username-input") }}</td>
</tr>
<tr>
<td>{{ form.age.label }}</td>
<td>{{ form.age() }}</td>
</tr>
<tr>
<td>{{ form.remember.label }}</td>
<td>{{ form.remember() }}</td>
</tr>
<tr>
<td>{{ form.tags.label }}</td>
<td>{{ form.tags() }}</td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="提交"/></td>
</tr>
</tbody>
</table>
</form>
</body>
</html>
通过创建WTForms表单类SettingsForm
,创建实例化对象form
之后,传递给模板。
传递过去的form
对象,通过form.属性
获取属性值,form.属性值.label
获取属性名。

如果想要添加属性class之类的,如下所示:
form.username(class="username-input")
安全表单
无需任何配置,Form
是一个带有 CSRF 保护的并且会话安全的表单。
但是如果想要禁用 CSRF 保护,可以这样:
form = Form(csrf_enabled=False)
如果想要全局禁用 CSRF 保护,真的不应该这样做。但是要坚持这样做的话,可以在配置中这样写:
WTF_CSRF_ENABLED = False
为了生成 CSRF 令牌,必须有一个密钥,这通常与Flask 应用密钥一致。如果想使用不同的密钥,可在配置中指定:
WTF_CSRF_SECRET_KEY = 'a random string'
或者
SECRET_KEY = 'a random string'
文件上传
jQuery 利用 FormData 上传文件
<form>
<input type="file" id="photo" name="photo">
<button type="button">保存</button>
</form>
但为什么只能选择一个文件??给<input>
添加一个multiple
属性就可以多选了!
<input type="file" id="photo" name="photo" multiple>
文件上传是Web开发中的重要话题,最直接和简单的方式是通过表单直接提交文件。 Harttle认为,我们引入jQuery来进行异步上传可以获得更好的用户体验。 一方面,在JavaScript中进行异步操作比表单更加灵活; 另一方面,异步上传也避免了上传大文件时的页面长时间卡死。
HTML
一个type=file
的<input>
就可以让用户来浏览并选择文件, 一般会把输入控件放到一个<form>
中,下面的一个简单的表单:
<form enctype="multipart/form-data">
<input type="file" id="photo" name="photo">
<button type="button">保存</button>
</form>
其中
enctype="multipart/form-data"
能够成功编码二进制数据。
但为什么我只能选择一个文件??给<input>
添加一个multiple
属性就可以多选了!
<input type="file" id="photo" name="photo" multiple>
获取文件列表
上述的<input>
将会拥有一个叫files
的DOM属性,包含了所选的文件列表(Array
)。
$('button').click(function(){
var $input = $('#photo');
// 相当于: $input[0].files, $input.get(0).files
var files = $input.prop('files');
console.log(files);
});
这个Array
中的每一项都是一个File
对象,它有下面几个主要属性:
name
: 文件名,只读字符串,不包含任何路径信息.size
: 文件大小,单位为字节,只读的64位整数.type
: MIME类型,只读字符串,如果类型未知,则返回空字符串.
jQuery上传文件
这是XMLHttpRequest Level 2提供的FormData
对象可以帮助进行二进制文件的 multipart/form-data
编码:
<script>
$(function () {
$('button').click(function () {
var files = $('#photo').prop('files');
console.log(files);
var data = new FormData();
data.append('photo', files[0]);
$.ajax({
url: '/upload/',
type: 'POST',
data: data,
cache: false,
processData: false,
contentType: false
});
});
})
</script>
upload.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
<script>
var csrftoken = $('meta[name=csrf-token]').attr('content')
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
})
</script>
</head>
<body>
<form action="{{url_for("upload")}}" method="POST" enctype="multipart/form-data">
<input type="file" id="photo" name="photo">
<button type="button">保存</button>
</form>
<script>
$(function () {
$('button').click(function () {
var files = $('#photo').prop('files');
console.log(files);
var data = new FormData();
data.append('photo', files[0]);
$.ajax({
url: '/upload/',
type: 'POST',
data: data,
cache: false,
processData: false,
contentType: false
});
});
})
</script>
</body>
</html>
创建表单类
class PhotoForm(Form):
photo = FileField('Your photo')
视图函数
@app.route('/upload/', methods=('GET', 'POST'))
@app.route('/', methods=('GET', 'POST'))
def upload():
if request.method == "GET":
return render_template('upload.html')
else:
form = PhotoForm(request.files)
if form.validate():
basepath = os.path.dirname(__file__) # 当前文件所在路径
# 注意:没有的文件夹一定要先创建
upload_path = os.path.join(basepath, r'.\uploads', secure_filename(form.photo.data.filename))
form.photo.data.save(upload_path)
return "{}".format(form.photo.data.filename)
else:
filename = None
return "{}".format(filename)
获取存储位置
考虑安全性,在保存文件前,先使用"werkzeug.utils.secure_filename"来对上传的文件名来进行过滤。
basepath = os.path.dirname(__file__) # 当前文件所在路径
# 注意:没有的文件夹一定要先创建
upload_path = os.path.join(basepath, r'.\uploads', secure_filename(form.photo.data.filename))
获取到上传文件后,使用form.photo.data.save(upload_path)
来保存文件
注意上传文件与传递参数的细节
上传文件时使用request.files
,传参数时使用request.form
验证上传的文件
定义表单的时候,对文件的字段,需要采用"FileField"这个类型
验证器应该从
flask_wtf.file
中导入,使用flask_wtf.file.FileRequired
是用来验证文件上传是否为空,flask_wtf.file.FileAllowed
用来验证上传的文件的后缀名
from wtforms import Form,FileField,StringField
from wtforms.validators import InputRequired
from flask_wtf.file import FileAllowed,FileRequired
class UploadFrom(Form):
# 使用Flask - WTF提供的FileRequired、FileAllowed验证函数
avatar = FileField(validators= [FileRequired(),FileAllowed(['jpg','png','gif'])])
desc = StringField(validators=[InputRequired()])
在视图文件中,想要将表单数据都传递过来(其中有传递的文件),就使用
from werkzeug.datastructures.CombinedMultiDict
,来把request.form
与request.files
来进行合并,再传给表单来做验证
如下:
form = UploadFrom(CombinedMultiDict([request.form,request.files]))
验证码
Flask-WTF 通过 RecaptchaField
也提供对验证码的支持:
from flask_wtf import Form, RecaptchaField
from wtforms import TextField
class SignupForm(Form):
username = TextField('Username')
recaptcha = RecaptchaField()
这伴随着诸多配置,你需要逐一地配置它们。
RECAPTCHA_PUBLIC_KEY
必须 公钥
RECAPTCHA_PRIVATE_KEY
必须 私钥
RECAPTCHA_API_SERVER
可选 验证码 API 服务器
RECAPTCHA_PARAMETERS
可选 一个 JavaScript(api.js)参数的字典
RECAPTCHA_DATA_ATTRS
可选 一个数据属性项列表 https://developers.google.com/recaptcha/docs/display
RECAPTCHA_PARAMETERS 和 RECAPTCHA_DATA_ATTRS 的示例:
RECAPTCHA_PARAMETERS = {'hl': 'zh', 'render': 'explicit'}
RECAPTCHA_DATA_ATTRS = {'theme': 'dark'}
对于应用测试时,如果 app.testing
为 True
,考虑到方便测试,Recaptcha 字段总是有效的。
CSRF 保护
为什么需要 CSRF?
Flask-WTF 表单保护免受 CSRF 威胁,不需要有任何担心。尽管如此,如果有不包含表单的视图,那么它们仍需要保护。
例如,由 AJAX 发送的 POST 请求,然而它背后并没有表单。在 Flask-WTF 0.9.0 以前的版本无法获得 CSRF 令牌。这是为什么要实现 CSRF。
SCRECT_KEY随机生成
import os
import base64
secret_key = os.urandom(44)
实现
为了能够让所有的视图函数受到 CSRF 保护,需要开启 CsrfProtec
模块:
from flask_wtf.csrf import CsrfProtect
CsrfProtect(app)
像任何其它的 Flask 扩展一样,可以惰性加载它:
from flask_wtf.csrf import CsrfProtect
csrf = CsrfProtect()
app = Flask(__name__)
csrf.init_app(app)
需要为 CSRF 保护设置一个秘钥。通常下,同 Flask 应用的 SECRET_KEY 是一样的。
如果模板中存在表单,你不需要做任何事情。与之前一样:
<form method="post" action="/">
{{ form.csrf_token }}
</form>
但是如果模板中没有表单,你仍然需要一个 CSRF 令牌:
<form method="post" action="/">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>
那么后端可用 request.form['csrf_token']
可以获取crsf_token
值。
无论何时未通过 CSRF 验证,都会返回 400 响应。可以自定义这个错误响应:
@csrf.error_handler
def csrf_error(reason):
return render_template('csrf_error.html', reason=reason), 400
强烈建议对所有视图启用 CSRF 保护。但也提供了某些视图函数不需要保护的装饰器:
@csrf.exempt
@app.route('/foo', methods=('GET', 'POST'))
def my_handler():
# ...
return 'ok'
默认情况下可以在所有的视图中禁用 CSRF 保护,通过设置 WTF_CSRF_CHECK_DEFAULT
为 False
,仅仅当需要的时候选择调用 csrf.protect()
。这也能够让在检查 CSRF 令牌前做一些预先处理:
@app.before_request
def check_csrf():
if not is_oauth(request):
csrf.protect()
AJAX
不需要表单,通过 AJAX 发送 POST 请求成为可能。0.9.0 版本后这个功能变成可用的。
假设已经使用了 CsrfProtect(app)
,可以通过 {{ csrf_token() }}
获取 CSRF 令牌。这个方法在每个模板中都可以使用,并不需要担心在没有表单时如何渲染 CSRF 令牌字段。
推荐的方式是在 <meta>
标签中渲染 CSRF 令牌:
<meta name="csrf-token" content="{{ csrf_token() }}">
在 <script>
标签中渲染同样可行:
<script type="text/javascript">
var csrftoken = "{{ csrf_token() }}"
</script>
下面的例子采用了在 <meta>
标签渲染的方式, 在 <script>
中渲染会更简单,无须担心没有相应的例子。
无论何时发送 AJAX POST 请求,为其添加 X-CSRFToken
头:
var csrftoken = $('meta[name=csrf-token]').attr('content')
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
})
那么后端可用 request.headers['X-CSRFToken']
可以获取crsf_token
值。
实例:
app.py
from flask import Flask,render_template,request
from flask_wtf import Form
from wtforms import StringField
from wtforms.validators import DataRequired,Length,EqualTo
from flask_wtf.csrf import CsrfProtect
app = Flask(__name__)
app.config["SECRET_KEY"] = "12345678"
crsf = CsrfProtect()
crsf.init_app(app)
....
if __name__ == '__main__':
app.run()
login.html
<!DOCTYPE html>
<html lang="en">
<head>
...
<meta name="csrf-token" content="{{ csrf_token() }}">
...
<script>
var csrftoken = $('meta[name=csrf-token]').attr('content')
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
})
</script>
</head>
<body>
...
<script>
$(function () {
$("#submit").click(function (event) {
{# 阻止默认事件发生#}
event.preventDefault()
var csrf_token = $("meta[name=csrf-token]").attr("content");
var name = $("form>input[name='name']").val()
var password1 = $("input[name='password1']").val()
var password2 = $("input[name='password2']").val()
$.ajax({
type: 'POST',
url: "/login/",
headers:{"X-CSRFToken":csrf_token},
data: {
"name": name,
"password1": password1,
"password2": password2,
},
success: function (data) {
console.log("post 请求成功")
},
});
})
})
</script>
</body>
</html>
最后更新于
这有帮助吗?