request.form能获取POST请求中提交的表单数据,但是为了某些重复操作,例如生成表单的HTML代码和验证提交的表单数据,使用Flask-WTF扩展。
pip install flask-wtf
4.1 跨站请求伪造保护
默认下,Flask-WTF能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery, CSRF)攻击。恶意网站把请求发送到被攻击者已登录的其他网站使就会引发CSRF攻击。
为了实现CSRF保护,Flask-WTF需要程序设置一个密钥,使用密钥生成加密令牌,再用令牌验证请求中表达数据的真伪。
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
此处app.config字典可以用来存储框架、扩展和程序本身的配置变量。SECRET_KEY配置变量是通用密钥,可在Flask和多个第三方扩展中使用。注意:为了安全性,密钥不要直接写入代码,而是存在环境变量。
4.2 表单类
使用Flask-WTF时,每个Web表单都由一个继承自Form的类表示。这个类定义表单中的一组字段,每个字段都用对象表示。字段对象可附属一个或多个验证函数。验证函数用来验证用户提交的输入值是否符合要求。
from flask_wtf import Form //flask.ext.wtf转换成flask_wtf
from wtforms import StringField, SubmitField
from wtforms.validators import Required
class NameForm(Form):
name = StringField('Whst is your name?', validators = [Required()])
submit = SubmitField('Submit')
此处代码为一个简单的Web表单,包含一个文本字段和一个提交按钮。NameForm表单中有一个名为name的文本字段和一个名为submit的提交按钮。StringField类表示属性为type=”text”的<input>元素,SubmitField类表示属性为type=”submit”的<input>元素。StringField构造函数的可选参数validators指定一个由验证函数组成的列表,在接受用户提交的数据之前验证数据。验证函数Required()确保提交字段不为空。
Form基类由Flask-WTF扩展定义,所以从flask_wtf导入。字段和验证函数直接从WTForms包中导入。
WTForms支持的HTML标准字段和验证函数自行查阅。
4.3 把表单渲染成HTML
视图函数把一个NameForm实例通过参数form传入模板,在模板中生成表单
<form method=”POST”>
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
可以为字段制定id或class属性,然后定义CSS样式
{{ form.name.label }} {{ form.name(id=’my-text-field’) }}
这种方式渲染表单的工作量较大,最好使用Bootstrap的表单样式
{% import “boostrap/wtf.html” as wtf %}
{{ wtf.quick_form(form) }}
4.4 在视图函数中处理表单
GET - 从指定的资源请求数据
POST - 向指定的资源提交要被处理的数据
@app.route('/', method=['GET','POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.data = ''
return render_template('index.html', form=form, name=name)
此处将表单提交作为POST请求处理更加便利。如果GET请求提交表单,由于GET请求没有主体,数据会以查询字符串的形式附加到URL中。
用户第一次访问程序时,服务器收到一个没有表单数据的GET请求,所以if语句部分跳过,通过渲染模板处理请求,并传入表单对象和值为None的name变量作为参数,用户在浏览器上看到一个表单。
用户提交表单后,服务器收到一个包含数据的POST请求。validate_on_submit()调用name字段上附属的Required()验证函数,如果名字不为空则验证通过,validate_on_submit()返回True。if语句中,name获得名字,data属性设为空字符串从而清空表单字段。最后调用render_template()函数渲染模板,参数name的值为表单中输入的名字。
hello.py
from flask import Flask, render_template
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
bootstrap = Bootstrap(app)
moment = Moment(app)
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.route('/', methods=['GET', 'POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.data = ''
return render_template('index.html', form=form, name=name)
app.run(host='0.0.0.0',debug=True)
index.html
{% 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 %}
4.5 重定向和用户会话
可用性问题:用户输入名字后提交表单,点击浏览器刷新按钮,会有再次提交表单警告。这是因为刷新页面时,浏览器会重新发送之前已经发送过的最后一个请求,而如果这个请求包含表单数据的POST请求,刷新页面会再次提交表单。
解决办法:POST/重定向/GET模式
使用重定向作为POST请求的响应,响应内容为URL而不是常规的包含HTML代码的字符串。浏览器收到重定向响应,会向重定向的URL发起GET请求,显示页面内容。而POST请求的form.name.data获取的输入名字将存储在用户会话中。
用户会话是一种私有存储,存在每个连接到服务器的客户端中,默认存在客户端的cookie中。
from flask import Flask, render_template, session, redirect, url_for
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
bootstrap = Bootstrap(app)
moment = Moment(app)
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'))
此处代码,原本的局部变量name现在保存在用户会话session[‘name’]中,所以两次请求之间能记住输入的值。合法表单数据的请求调用redirect()函数,参数为重定向的地址。url_for()默认唯一必须参数是路由的内部名字,默认为视图函数的名字,这里指index()。render_template()函数使用session.get(‘name’)直接从会话中读取name参数。get()获取字典中键对应的值,若无返回none。
4.6 Flask消息
请求完成后,响应消息提示用户输入是否正确。使用flash()函数,在发给客户端的下一个响应中显示一个消息。
hello.py
@app.route('/', methods=['GET', 'POST'])
def index():
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('Looks 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()函数,在发给客户端的下一个响应中显示一个消息。
base.html(建议在基模板中渲染)
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
此处代码使用了for循环,因为在之前的请求循环中,每次调用flash()都会生成一个消息,所以可能有多个消息在排队等待显示。get_flashed_messages()函数获取的消息在下次调用时不会再次返回,因此Flash消息只显示一次。