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 %}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,825评论 6 546
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,814评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,980评论 0 384
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 64,064评论 1 319
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,779评论 6 414
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,109评论 1 330
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,099评论 3 450
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,287评论 0 291
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,799评论 1 338
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,515评论 3 361
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,750评论 1 375
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,221评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,933评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,327评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,667评论 1 296
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,492评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,703评论 2 380

推荐阅读更多精彩内容