Flask Web 开发【 Chapter 4 】 Web 表单

【Chapter 4】 Web 表单

4.1 跨站请求伪造保护

默认情况下,Flask-WTF 能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery, CSRF)的攻击。恶意网站把请求发送到被攻击者已登录的其他网站时就会引发 CSRF 攻击。

为了实现 CSRF 保护,Flask-WTF 需要程序设置一个密钥。Flask-WTF 使用这个密钥生成 加密令牌,再用令牌验证请求中表单数据的真伪。

hello.py 设置 Flask-WTF

# 设置 Flask-WTF
app.config['SECRET_KEY'] = 'hard tp guess string'

app.config 字典可用来存储框架、扩展和程序本身的配置变量。使用标准的字典句法就能把配置值添加到 app.config 对象中。这个对象还提供了一些方法,可以从文件或环境中导入配置值。

SECRET_KEY 配置变量是通用密钥,可在 Flask 和多个第三方扩展中使用。如其名所示,加密的强度取决于变量值的机密程度。不同的程序要使用不同的密钥,而且要保证其他人不知道你所用的字符串。

4.2 表单类

使用 Flask-WTF 时,每个 Web 表单都由一个继承自 Form 的类表示。这个类定义表单中的 一组字段,每个字段都用对象表示。字段对象可附属一个或多个验证函数。验证函数用来验证用户提交的输入值是否符合要求。

一个简单的 Web 表单,包含一个文本字段和一个提交按钮。修改 hello.py

from flask_wtf import Form
from wtforms import StringField,SubmitField
from wtforms.validators import DataRequired

class NameForm(Form):
    # Required 就是 DataRequired。Required 已经因为命名可能造成歧义而被废弃了,不推荐在新项目中使用,目前还能使用是为了兼容老版本。
    name = StringField('What is your name?',validators=[DataRequired()])
    submit = SubmitField('Submit')

这个表单中的字段都定义为类变量,类变量的值是相应字段类型的对象。

NameForm 表单中有一个名为 name 的文本字段和一个名为 submit 的提交按钮。StringField 类表示属性为 type="text" 的 <input> 元素。SubmitField 类表示属性为 type="submit" 的 <input> 元素。字段构造函数的第一个参数是把表单渲染成 HTML 时使用的标号。

StringField 构造函数中的可选参数 validators 指定一个由验证函数组成的列表,在接受用户提交的数据之前验证数据。验证函数 DataRequired() 确保提交的字段不为空。

WTForms支持的HTML标准字段如表所示

WTForms 内建的验证函数如表所示

4.3 把表单渲染成 HTML

表单字段是可调用的,在模板中调用后会渲染成 HTML。

若视图函数把一个 NameForm 实例通过参数 form 传入模板,在模板中可以生成一个简单的表单,如下所示:

<form method="POST">
    {{ form.hidden_tag() }}
    {{ form.name.label }}{{ form.name(id='my-text-field') }}
    {{ form.submit() }}
</form>

要想改进表单外观,可以把参数传入渲染字段的函数,传入的参数会被转换成字段的 HTML 属性。例如:

form.name(id='my-text-field') 为 name 字段 指定了 id 或 class 属性,,然后定义 CSS 样式。

即便能指定 HTML 属性,但按照这种方式渲染表单的工作量还是很大,所以在条件允许的 情况下最好能使用 Bootstrap 中的表单样式。上述表单可以使用下面的方式渲染:

{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}

导入的 bootstrap/wtf.html 文件中定义了一个使用Bootstrap 渲染Falsk-WTF 表单对象的辅助函数。wtf.quick_form() 函数的参数为 Flask-WTF 表单对象,使用 Bootstrap 的默认样式渲染传入的表单。

hello.py 的完整模板如下所示:templates\index.html:使用 Flask-WTF 和 Flask-Bootstrap 渲染表单

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

{% block title %}Flasky{% endblock %}

{% block page_content %}
    <div class="page-header">
        <h1>
            Hello,{% if  name %}{{ name }}{% else %}Stranger{% endif %}!
        </h1>
    </div>
    {{ wtf.quick_form(form) }}
{% endblock %}

Jinja2 中的条件语句格式为 {% if condition %}...{% else %}...{% endif %}。 如果条件的计算结果为 True,那么渲染 if 和 else 指令之间的值。如果条件的计算结果为 False,则渲染 else 和 endif 指令之间的值。

在这个例子中,如果没有定义模板变量 name,则会渲染字符串“Hello, Stranger!”。内容区的第二部分使用 wtf.quick_form() 函数渲染 NameForm 对象。

4.4 在视图函数中处理表单

在新版 hello.py 中,视图函数 index() 不仅要渲染表单,还要接收表单中的数据。

@app.route('/', methods=['GET', 'POST'])
def index():
    name = None
    form = NameForm()
    if form.validate_on_submit():
        # 用户输入的名字可通过字段的 data 属性获取
        name = form.name.data
        # 清空表单字段
        form.name.data = ''
    # 渲染模板
    return render_template('index.html', form=form, name=name)

@app.route('/', methods=['GET', 'POST'])添加的 methods 参数告诉 Flask 在 URL 映射中把这个视图函数注册为 GET 和 POST 请求的处理程序。如果没指定 methods 参数,就只把视图函数注册为 GET 请求的处理程序。

局部变量 name 用来存放表单中输入的有效名字,如果没有输入,其值为 None。如上述代码所示,在视图函数中创建一个 NameForm 类实例用于表示表单。提交表单后,如果数据能被所有验证函数接受,那么 validate_on_submit() 方法的返回值为 True,否则返回 False。 这个函数的返回值决定是重新渲染表单还是处理表单提交的数据。

用户第一次访问程序时,服务器会收到一个没有表单数据的 GET 请求,所以 validate_on_ submit() 将返回 False。if 语句的内容将被跳过,通过渲染模板处理请求,并传入表单对象和值为 None 的 name 变量作为参数。用户会看到浏览器中显示了一个表单。

用户提交表单后,服务器收到一个包含数据的 POST 请求。validate_on_submit() 会调用 name 字段上附属的 DataRequired() 验证函数。如果名字不为空,就能通过验证,validate_on_ submit() 返回 True。

现在,用户输入的名字可通过字段的 data 属性获取(name = form.name.data)。在 if 语句中, 把名字赋值给局部变量 name,然后再把 data 属性设为空字符串,从而清空表单字段。最后一行调用 render_template() 函数渲染模板,但这一次参数 name 的值为表单中输入的名 字,因此会显示一个针对该用户的欢迎消息。

首次进入页面

提交正确表单

提交空表单(不填写字段)

提交空表单(" ")

4.5 重定向和用户会话

出现的问题:最新版的 hello.py 存在一个可用性问题。用户输入名字后提交表单,然后点击浏览器的刷 新按钮,会看到一个莫名其妙的警告,要求在再次提交表单之前进行确认。

原因:是因为刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。如果这个请求是一个包含表单数据的 POST 请求,刷新页面后会再次提交表单。

解决方法:别让 Web 程序把 POST 请 求作为浏览器发送的最后一个请求。

实现方式:使用重定向作为 POST 请求的响应,而不是使用常规响应。

重定向:重定向是一种特殊的响应,响应内容是 URL,而不是包含 HTML 代码的字符串。浏览器收到这种响应时,会向重定向的 URL 发起 GET 请求,显示页面的内容。

现在,最后一个请求是 GET 请求,所以刷新命令能像预期的那样正常使用了。这个技巧称为 Post/ 重定向 /Get 模式。

但这种方法会带来另一个问题。程序处理 POST 请求时,使用 form.name.data 获取用户输入的名字,可是一旦这个请求结束,数据也就丢失了。因为这个 POST 请求使用重定向处理,所以程序需要保存输入的名字,这样重定向后的请求才能获得并使用这个名字,从而构建真正的响应。

用户会话是一种私有存储,存在于每个连接到服务器的客户端中。程序可以把数据存储在用户会话中,在请求之间“记住”数据。

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

hello.py:重定向和用户会话

from flask import Flask, render_template, session, redirect, url_for

@app.route('/', methods=['GET', 'POST'])
def index():
    name = None
    form = NameForm()
    if form.validate_on_submit():
        # name = form.name.data
        # form.name.data = ''
        # 局部变量 naem 保存在 session 中
        session['name'] = form.name.data
        # redirect() 参数是一个 URl
        # url_for() 函数的第一个且唯一必须指定的参数是端点名,即路由的内部名字。
        return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name'))

局部变量 naem 保存在 session 中,即 session['name'],所以在两次请求之间也能记住输入的值。

redirect() 用来生成 HTTP 重定向响应。函数的参数是重定向的 URL,推荐使用 url_for() 生成 URL,因为这个函数使用 URL 映射生成 URL,从而保证 URL 和定义的路由兼容,而且修改路由名字后 依然可用。

url_for() 函数的第一个且唯一必须指定的参数是端点名,即路由的内部名字。默认情 况下,路由的端点是相应视图函数的名字。在这个示例中,处理根地址的视图函数是 index(),因此传给 url_for() 函数的名字是 index。

render_function() 函数中,使用 session.get('name') 直接从会话中读取 name 参数的值。和普通的字典一样,这里使用 get() 获取字典中键对应的值以避免未找到键的异常情况,因为对于不存在的键,get() 会返回默认值 None。

这样刷新的时候就不会出现莫名的警告了。

4.6 Flash 消息

请求完成后,有时需要让用户知道状态发生了变化(可以使用确认消息、警告或者错误提醒)。这种功能是 Flask 的核心特性,flash() 函数可实现这种效果。

hello.py:Flash 消息

def index():
    # name = None
    form = NameForm()
    if form.validate_on_submit():
        old_name = session.get('name')
        if old_name is not None and old_name != form.name.data:
            flash('Look like you have changed your name!')
        session['name'] = form.name.data
        return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name'))

仅调用 flash() 函数并不能把消息显示出来,程序使用的模板要渲染这些消息。最好在基模板中渲染 Flash 消息,因为这样所有页面都能使用这些消息。Flask 把 get_flashed_ messages() 函数开放给模板,用来获取并渲染消息。

修改 base.html:渲染 Flash 消息

{% block content %}
    <div class="container">
        {% for message in get_flashed_messages() %}
        <div class="alert alert-warning">
            <button type="button" class="close" data-dismiss="alert">&times;</button>
            {{ message }}
        </div>
        {% endfor%}
        {% block page_content %}{% endblock %}
    </div>
{% endblock %}

在模板中使用循环是因为在之前的请求循环中每次调用 flash() 函数时都会生成一个消息, 所以可能有多个消息在排队等待显示。

get_flashed_messages() 函数获取的消息在下次调用时不会再次返回,因此 Flash 消息只显示一次,然后就消失了。

最后显示页面

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容