作用
与 Flask-Uploads 一起支持文件上传。
安装
导入
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.
表单定义
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
。
常用字段
接收给定格式datetime.datetime的文本框
验证表单
from wtforms.validators import *
验证数据是否真实存在,即不能为空,必须是非空白字符串,否则触发StopValidation错误。
比较两个域的值;在要求输入两次密码进行确认的时候非常有 用
数据发过来时,经过表单验证,因此需要验证器来进行验证,以下对一些常用的内置验证器进行讲解:
# 验证邮箱
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)])
# 正则表达式
phone = StringField(validators=[Regexp(r'1[34578]\d{9}')])
# url验证
home_page = StringField(validators=[URL()])
# 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)])
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 保护的并且会话安全的表单。
form = Form(csrf_enabled=False)
如果想要全局禁用 CSRF 保护,真的不应该这样做。但是要坚持这样做的话,可以在配置中这样写:
WTF_CSRF_ENABLED = False
为了生成 CSRF 令牌,必须有一个密钥,这通常与Flask 应用密钥一致。如果想使用不同的密钥,可在配置中指定:
WTF_CSRF_SECRET_KEY = 'a random string'
或者
SECRET_KEY = 'a random string'
文件上传
<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()
这伴随着诸多配置,你需要逐一地配置它们。
可选 一个 JavaScript(api.js)参数的字典
可选 一个数据属性项列表 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>