第四章 Web表單
序:為什么需要Flask-wtf
第 2 章中介紹的請求對象包含客戶端發(fā)出的所有請求信息。
其中, request.form 能獲取POST 請求中提交的表單數(shù)據(jù)。
盡管 Flask 的請求對象提供的信息足夠用于處理 Web 表單,
但有些任務很單調,而且要重復操作。
比如,生成表單的 HTML 代碼和驗證提交的表單數(shù)據(jù)。
Flask-WTF( http://pythonhosted.org/Flask-WTF/) 擴展可以把處理 Web 表單的過程變成一種愉悅的體驗。
這個擴展對獨立的 WTForms( http://wtforms.simplecodes.com)包進行了包裝,方便集成到 Flask 程序中。
Flask-WTF 及其依賴可使用 pip 安裝:
(venv) $ pip install flask-wtf
4.1 跨站請求偽造保護
為何需要CSRF保護
默認情況下, Flask-WTF 能保護所有表單免受跨站請求偽造
( Cross-Site Request Forgery,CSRF)的攻擊。
惡意網站把請求發(fā)送到被攻擊者已登錄的其他網站時就會引發(fā) CSRF 攻擊。
為了實現(xiàn) CSRF 保護,
Flask-WTF 需要程序設置一個密鑰。
如何設置密鑰
Flask-WTF 使用這個密鑰生成加密令牌,
再用令牌驗證請求中表單數(shù)據(jù)的真?zhèn)巍?br>
例:hello.py: 設置 Flask-WTF
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config 字典可用來存儲框架、擴展和程序本身的配置變量。
使用標準的字典句法就能把配置值添加到 app.config 對象中。
這個對象還提供了一些方法,
可以從文件或環(huán)境中導入配置值。
SECRET_KEY 配置變量是通用密鑰,
可在 Flask 和多個第三方擴展中使用。
如其名所示,加密的強度取決于變量值的機密程度。
不同的程序要使用不同的密鑰,
而且要保證其他人不知道你所用的字符串。
為了增強安全性,密鑰不應該直接寫入代碼,
而要保存在環(huán)境變量中。這一技術會在第 7 章介紹。
4.2 表單類
表單的結構
使用 Flask-WTF 時,
每個 Web 表單都由一個繼承自 Form 的類表示。
這個類定義表單中的一組字段,
每個字段都用對象表示。
字段對象可附屬一個或多個驗證函數(shù)。
驗證函數(shù)用來驗證用戶提交的輸入值是否符合要求。
一個簡單的web表單
from flask_wtf import FlaskForm # 0.13開始不推薦原書的Form
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired # 原書是Required,官網最新示例為DataRequired
class NameForm(Form):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
在這個示例中,
NameForm 表單中有一個名為 name 的文本字段
和一個名為 submit 的提交按鈕。
WTForms支持的HTML標準字段
字段類型 | 說明 |
---|---|
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密碼文本字段 |
HiddenField | 隱藏文本字段 |
DateField | 文本字段,值為 datetime.date 格式 |
DateTimeField | 文本字段,值為 datetime.datetime 格式 |
IntegerField | 文本字段,值為整數(shù) |
DecimalField | 文本字段,值為 decimal.Decimal |
FloatField | 文本字段,值為浮點數(shù) |
BooleanField | 復選框,值為 True 和 False |
RadioField | 一組單選框 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可選擇多個值 |
FileField | 文件上傳字段 |
SubmitField | 表單提交按鈕 |
FormField | 把表單作為字段嵌入另一個表單 |
FieldList | 一組指定類型的字段 |
WTForms驗證函數(shù)
驗證函數(shù) | 說明 |
---|---|
驗證電子郵件地址 | |
EqualTo | 比較兩個字段的值;常用于要求輸入兩次密碼進行確認的情況 |
IPAddress | 驗證 IPv4 網絡地址 |
Length | 驗證輸入字符串的長度 |
NumberRange | 驗證輸入的值在數(shù)字范圍內 |
Optional | 無輸入值時跳過其他驗證函數(shù) |
Required | 確保字段中有數(shù)據(jù) |
Regexp | 使用正則表達式驗證輸入值 |
URL | 驗證 URL |
AnyOf | 確保輸入值在可選值列表中 |
NoneOf | 確保輸入值不在可選值列表中 |
4.3 把表單渲染成HTML
如何調用上節(jié)的NameForm表單
可以通過參數(shù)form傳入模板,例如:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
<form>
標簽是html語言用來顯示表單的,
而縮進部分的form則是傳入的參數(shù),
是由hello.py中的render_temlate傳進模板的。
如何渲染表單
這個表單還很簡陋。要想改進表單的外觀,
可以為字段指定 id 或 class 屬性,
然后在CSS樣式表里改變對應id或class的外觀:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>
hidden_tag用來渲染所有的隱藏Field。
為什么使用Flask-Bootstrap渲染更好
即便能指定 id 或 class 屬性,
但按照這種方式渲染表單的工作量還是很大,
所以在條件允許的情況下最好能使用 Bootstrap 中的預定義表單樣式。
Flask-Bootstrap 使用預定義樣式渲染整個 Flask-WTF 表單,
只需一次調用即可完成。
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
wtf.quick_form() 函數(shù)的參數(shù)為 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 %}
模板的內容區(qū)(page_content)現(xiàn)在有兩部分。
第一部分是頁面頭部(page_header),
第二部分使用 wtf.quick_form() 渲染上節(jié)的NameForm 實例。
這個程序必須和下節(jié)重定義的index()一起使用才行。
Jinja2 中的條件語句格式為 {% if condition %}...{% else %}...{% endif %}。
4.4 在視圖函數(shù)中處理表單
更新后的index()視圖函數(shù)
@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)
可以看到在路由中,多了一個methods,
后面除了默認值GET,還多了POST方法,
可以在網上搜下RESTFUL的簡單說明。
NameForm的實例form會被Flask-Bootstrap渲染成網頁上的表單,
只有當用戶在表單中提交數(shù)據(jù)時,
才會執(zhí)行if嵌套的語句,調用POST方法提交數(shù)據(jù),
否則只是調用GET方法,顯示空表單。
沒必要把request.form(4.3第一個示例,form標簽內容)傳給Flask-wtf,
它自己會自動讀取。
并且validate_on_submit會檢查是否是POST,并且是否是有效數(shù)據(jù)。
因為每個表單都必須提交(submit),
所以validate_on_submit用來確認所有數(shù)據(jù)段的驗證通過。
4.5 重定向和用戶會話
POST后再刷新頁面會出現(xiàn)警告
最新版的 hello.py 存在一個可用性問題。
用戶輸入名字后提交表單,
然后點擊瀏覽器的刷新按鈕,
會看到一個莫名其妙的警告,
要求在再次提交表單之前進行確認。
之所以出現(xiàn)這種情況,
是因為刷新頁面時瀏覽器會重新發(fā)送之前已經發(fā)送過的最后一個請求。
如果這個請求是一個包含表單數(shù)據(jù)的 POST 請求,
刷新頁面后會再次提交表單。
大多數(shù)情況下,這并不是理想的處理方式。
如何避免刷新時POST作為最后一個請求:重定向
很多用戶都不理解瀏覽器發(fā)出的這個警告。
基于這個原因,
最好別讓 Web 程序把 POST請求作為瀏覽器發(fā)送的最后一個請求。
既然最后一個請求不能是POST方法,
可以嘗試在POST后自動添加一個GET方法,
這個GET方法用原來的參數(shù)重新獲取當前頁面。
這種需求的實現(xiàn)方式是使用重定向,
會在把POST+GET封裝為一個GET請求,
刷新命令也就能像預期那樣使用了。
重定向時會丟失原有的輸入數(shù)據(jù)
但這種方法會帶來另一個問題。
程序處理 POST 請求時,
使用 form.name.data 獲取用戶輸入的名字,
可是一旦這個請求結束,
數(shù)據(jù)也就丟失了。
如果沒有用戶輸入的數(shù)據(jù),
重定向后的頁面就如同用戶沒有輸入。
如何保存這些數(shù)據(jù):用戶會話
程序可以把數(shù)據(jù)存儲在用戶會話中,
在請求之間“ 記住”數(shù)據(jù)。
每個連接到服務器的客戶端中都有不同的用戶會話。
我們在第 2 章介紹過用戶會話,
它是請求上下文中的變量,
名為 session,
像標準的 Python 字典一樣操作。
默認情況下,
用戶會話保存在客戶端 cookie 中,
使用設置的 SECRET_KEY 進行加密簽名。
如果篡改了 cookie 中的內容,
簽名就會失效,會話也會隨之失效。
使用重定向和用戶會話的Index函數(shù)
from flask import Flask, render_template, session, redirect, url_for
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect('/')
return render_template('index.html', form=form, name=session.get('name'))
新的Index函數(shù)以用戶會話存儲輸入的數(shù)據(jù),
然后重定向到當前URL,
再把存儲好的數(shù)據(jù)發(fā)送給URL對應的模板。
即用session['name']
存儲form.name.data
,
然后redirect到index對應的URL'/'
,
在把session['name']
發(fā)送給'/'
對應的模板。
推薦在重定向時,
使用redirect(url_for('index'))代替redirect('/'),
這樣只要不改動index這個名字,
即使改動index對應的URL,
也可以正確地重定向。
form.name.data能夠獲取表單中name的值,
而session.get('name')則直接從會話中讀取name的值。
使用session.get('name')而不是session['name']獲取name的值,
可以避免發(fā)生未找到鍵的異常,
對于不存在的鍵,
get()會返回默認值None,
這點和普通字典一樣。
4.6 Flash消息
為什么需要Flask消息
請求完成后,有時需要讓用戶知道狀態(tài)發(fā)生了變化。
這里可以使用確認消息、警告或者錯誤提醒。
一個典型例子是,
用戶提交了有一項錯誤的登錄表單后,
服務器發(fā)回的響應重新渲染了登錄表單,
并在表單上面顯示一個消息,
提示用戶用戶名或密碼錯誤。
這種功能是 Flask 的核心特性。
如下例所示,
flash() 函數(shù)可實現(xiàn)這種效果。
hello.py:Flash消息
from flask import Flask, render_template, session, redirect, url_for, flash
@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'))
在這個示例中,
每次提交的名字都會和存儲在用戶會話中的名字進行比較,
而會話中存儲的名字是前一次在這個表單中提交的數(shù)據(jù)。
如果兩個名字不一樣,
就會調用 flash() 函數(shù),
在發(fā)給客戶端的下一個響應中顯示一個消息。
Flash消息暫時不能顯示:需要渲染
僅調用 flash() 函數(shù)并不能把消息顯示出來,
程序使用的模板要渲染這些消息。
最好在基模板中渲染 Flash 消息,
因為這樣所有頁面都能使用這些消息。
Flask 把 get_flashed_messages() 函數(shù)開放給模板,
用來獲取并渲染消息,如下例所示。
渲染 Flash 消息:templates/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 %}
在模板中使用循環(huán)是因為在之前的請求循環(huán)中,
每次調用 flash() 函數(shù)時都會生成一個消息,
所以可能有多個消息在排隊等待顯示。
get_flashed_messages() 函數(shù)獲取的消息在下次調用時不會再次返回,
因此 Flash 消息只顯示一次,然后就消失了。
在這個示例中,
使用 Bootstrap 提供的警報 CSS 樣式渲染警告消息。