6.表单WTForms

表单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>

image-20200905000329099

image-20200905000346166

问题

添加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 *
验证程序描述.
Email验证邮箱地址
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获取属性名。

image-20200905002211082

如果想要添加属性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

验证上传的文件
  1. 定义表单的时候,对文件的字段,需要采用"FileField"这个类型

  2. 验证器应该从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()])
  1. 在视图文件中,想要将表单数据都传递过来(其中有传递的文件),就使用from werkzeug.datastructures.CombinedMultiDict,来把request.formrequest.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_PARAMETERSRECAPTCHA_DATA_ATTRS 的示例:

RECAPTCHA_PARAMETERS = {'hl': 'zh', 'render': 'explicit'}
RECAPTCHA_DATA_ATTRS = {'theme': 'dark'}

对于应用测试时,如果 app.testingTrue ,考虑到方便测试,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_DEFAULTFalse,仅仅当需要的时候选择调用 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>
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值