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消息只顯示一次。