Flask系列:表单

这个系列是学习《Flask Web开发:基于Python的Web应用开发实战》的部分笔记

网站需要能提供一个表格,让用户提供信息进行注册、写点东西

处理POST请求中提交的表单数据

  • 用 flask 的请求对象,request.form,但功能很初级,需要做很多重复、额外的操作
  • 一个名为 Flask-WTF 的扩展,将 WTForms 集成到 flask 程序,可以帮助完成很多事情

CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击

恶意网站在受害者不知情的情况下,伪造请求,以受害者名义(利用用户浏览器中的 cookie )发送给受害者已登录的受攻击站点,在自身没有授权的情况下执行用户的某些权限的操作。

IBM CSRF
wiki CSRF
wiki cookie

为了进行防御,需要在响应的表单(不在 cookie 中)中添加一个攻击者无法伪造的信息,即随机产生的token,然后在提交表单的请求中送回,进行比对验证

在程序的配置中设置一个密钥,启用 CSRF 保护

CSPR_ENABLED = True # 启用 CSPR (跨站请求伪造) 保护,在表单中使用,隐藏属性
SECRET_KEY = 'this-is-safe-and-you-never-guess-it' # 建立一个用于加密的密钥,验证表单

使用:

如果是使用 Flask-WTF 自动化表单的一些操作,会自动用密钥生成一个随机的加密令牌,放入响应中,并要求在用户提交的请求中送回,通过对比是否一致,判断表单的真伪

如果是手动写,添加

    {{ form.hidden_tag() }}

表单类

表单的创建,可以通过继承从 Flask-WTF 导入的Form父类实现

from flask.ext.wtf import Form # 表单类,从第三方扩展的命名空间 导入

表单类中需要定义 属性/字段,值是字段类型类,就是将要在 HTML 中显示的表单各个字段,其实就是对 HTML 表单各种标签的包装

from wtforms import StringField, BooleanField, SubmitField, PasswordField, TextAreaField, SelectField # 字段类型类,字符串、布尔值、提交、密码、文本区域、选择框

字段类型类(说明文本,验证器列表)

验证器列表,检查用户填写表单时输入的内容是否符合我们的期望,有多个验证器时,需要同时通过验证

from wtforms.validators import DataRequired, Required, Length, Email  , Regexp, EqualTo # 验证器,直接从 wtforms.validators 导入
# 普通用户的资料编辑表单
class EditProfileForm(Form):
    name = StringField('Real name', validators=[Length(0, 64)]) # 因为是可选,允许长度为0
    location = StringField('Location', validators=[Length(0, 64)])
    about_me = TextAreaField('About me') # 文本区域,可以多行,可以拉动
    submit = SubmitField('Submit')

可以在 表单类 中,通过定义validate_开头的类方法,自定义验证器,用ValidationError定义报错的提示信息。自定义的验证器会和在用户提交表单时自动被调用

from wtforms import ValidationError


# 管理员的资料编辑表单
class EditProfileAdminForm(Form):
    # 检查提交的昵称
    # 如果字段值没有变,跳过验证
    # 如果新的与旧的不同,但与其他用户的昵称冲突,报错
    # 如果有变化,且与其他用户不冲突,验证通过
    def validate_username(self, field):
        if field.data != self.user.username and User.query.filter_by(username=field.data).first():
            raise ValidationError('Username already in use.')

渲染

将表单渲染成 HTML

如果是 WTF()

{% import "bootstrap/wtf.html" as wtf %}

{{ wtf.quick_form(form) }}  # 渲染时,将 form 作为参数传递给模板

如果是手动写

<form method="POST"> # 表单提交方式为 POST
    {{ form.hidden_tag() }}
    {{ form.name.label }} {{ form.name() }}
    {{ form.submit() }}
</form>

在视图中的处理

导入定义的表单类

from .forms import  EditProfileForm

在匹配 URL 和 HTTP 的请求方式时,需要添加 POST 方法,默认只处理 GET 请求

其实 HTTP 中 GET方式 和 POST方式 都可以提交表单中填写的数据,区别是,GET方式会将数据以查询字符串的形式放到 URL 中提交,POST方式 会将数据保存在 HTTP 主体中提交

# 个人主页编辑页面
@main.route('/edit_profile', methods=['GET', 'POST'])

实例化表单类

    form = EditProfileForm()

查看提交的数据是否能被所有验证器验证通过,如果通过,通过form.字段名.data获取指定字段的内容,并保存到数据库,否则,设置表单字段为当前值(如果是修改或编辑页面),或直接返回空表单(注册、登陆 页面)

    if form.validate_on_submit():
        current_user.name = form.name.data
        current_user.location = form.location.data
        current_user.about_me = form.about_me.data
        db.session.add(current_user)
        db.session.commit()
        flash('Your profile has been updated')
        return redirect(url_for('.user', username=current_user.username)) # 提交后,转到个人主页,显示编辑结果
    # 如果是 GET,或 验证器不通过,显示目前的资料内容
    form.name.data = current_user.name 
    form.location.data = current_user.location
    form.about_me.data = current_user.about_me
    return render_template('edit_profile.html', form=form)

登陆页面的例子:

from .forms import LoginForm

# 登录页面,填写表单、认证
@auth.route('/login', methods = ['GET', 'POST']) # 接收 url 为 `/login`, HTTP 方式为 'GET' 和 'POST' 的请求
def login():
    form = LoginForm() # 创建实例,表示表单
    if form.validate_on_submit(): # 如果 通过 post 提交的表单,数据通过了所有验证器的检查
        user = User.query.filter_by(email=form.email.data).first() 
        if user is not None and user.verify_password(form.password.data): 
            login_user(user, form.remember_me.data) 
            return redirect(request.args.get('next') or  url_for('main.index'))
        flash('Invalid username or password.')

    return render_template('auth/login.html', form=form)

刷新

如果提交表单后,点击浏览器刷新按钮,会要求在浏览器再次提交表单前进行确认

这是因为,刷新页面时,浏览器会重新发送最后一次发送过的请求

为了避免用户遇到这种情况,需要避免让 POST 请求作为浏览器最后发出的一个请求

POST/重定向/GET 模式

对于用户的 POST 请求,如果验证通过,使用重定向作为响应,使得浏览器向 响应中的重定向URL 发送 GET 请求,这样就可以正常刷新了

返回重定向响应的方法

return redirect(url_for('auth.login')) 使用redirect()函数,将目标 URL 作为参数

# 注册页面
@auth.route('/register', methods = ['GET', 'POST']) # 初次 get 请求获取空白表单,然后 post 请求提交填写后的表单
def register():
    form = RegistrationForm()
    if form.validate_on_submit():

        return redirect(url_for('auth.login')) # 重定向到登陆页面, 让用户登陆。浏览器向 login url 发送 get 请求。
    return render_template('auth/register.html', form=form)

通用密钥

SECRET_KEY 不仅可以用于 CSRF 还可以用于加密 cookie

cookie 是网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据

默认情况下,用户会话保存在客户端 cookie 中,使用设置的 SECRET_KEY 进行加密签名。如果篡改了 cookie 中的内容,签名就会失效,会话也会随之 失效。

在下一个返回的响应中显示当前处理结果的消息

请求完成后,有时需要让用户知道状态发生了变化。

通过函数flash()get_flashed_messages(),可以将当前请求处理的结果,在下一个返回的响应中显示

两步:

  • 在 view 中,用函数flash()定义、收集消息
# 退出,跳转到主页
@auth.route('/logout')
@login_required # 保护路由,只允许已登陆用户访问。Flask-Login 提供的装饰器,将用户重定向到 登陆页面
def logout():
    logout_user() # 删除并重设用户会话
    flash('You have been logged out.')
    return redirect(url_for('main.index'))

当前处理的响应是重定向URL,然后在浏览器向 重定向的URL 请求的响应中显示

需要注意,可以多次调用 flash() 收集多条消息,形成队列,但所有消息都只能显示一次

  • 在模板中调用函数get_flashed_messages()显示所有收集的消息

因为可能队列中有多条消息,所以需要用 for 循环获取

    {% for message in get_flashed_messages() %} 
         {{ message }}
    {% endfor %}
    原文作者:超net
    原文地址: https://www.jianshu.com/p/3b8d0d961fd3
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞