表單是讓用戶與我們的網頁應用程序交互的基本元素腰奋。Flask 本身并不會幫助我們處理表單砌函,但是 Flask-WTF 擴展讓我們在我們的 Flask 應用程序中使用流行的 WTForms 包祭埂。這個包使得定義表單和處理提交容易一些茶敏。
Flask-WTF
我們想要使用 Flask-WTF 做的第一件事情(在安裝它以后)就是在 myapp.forms
包中定義一個表單迈螟。
# ourapp/forms.py
from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email
class EmailPasswordForm(Form):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
- 在 Flask-WTF 0.9 版本以前抖棘,Flask-WTF 提供了針對 WTForms 字段以及驗證器的自己的封裝。你可能看到外面一大堆的代碼是從
flask.ext.wtforms
中不是從wtforms
中導入TextField
净捅,PasswordField
疑枯。 - 在 Flask-WTF 0.9 版本以后,我們應該直接從
wtforms
中導入這些字段和驗證器蛔六。
我們定義的表單是一個用戶登錄表單荆永。我們把它叫做 EmailPasswordForm()
,我們可以重用這個同樣的表單類(Form
)去做其它的一些事情国章,像注冊表單具钥。這里我們沒有去定義一個又長又沒有用的表單,而是選擇一個很常用的表單液兽,只是為了給你們介紹使用 Flask-WTF 定義表單的方式骂删。也許以后在正式項目中會定義一個特別復雜表單掌动。對于表單中包含字段名稱,我們建議使用一個清楚的名稱宁玫,并且在一個表單中保持唯一粗恢。不得不說,對于一個長的表單欧瘪,我們可能要給出一個更符合上文的字段名稱眷射。
登錄表單可以替我們做一些事情。它能夠保證我們應用程序的安全以防止 CSRF 漏洞佛掖,驗證用戶輸入并且渲染適當的標記妖碉,這些標記是我們?yōu)楸韱味x的字段。
CSRF 保護和驗證
CSRF 表示跨站請求偽造芥被。CSRF 攻擊是指第三方偽造(像一個表單提交)請求到一個應用程序的服務器欧宜。一個易受攻擊的服務器假設從一個表單來的數據是來自它自己的網站并且采取相應的操作。
作為一個例子拴魄,比方說冗茸,一個郵件提供商可以讓你通過提交一個表單來刪除你的賬號。表單發(fā)送一個 POST 請求到服務器上的 account_delete
端點并且當表單被提交的時候刪除登錄的賬號羹铅。我們可以在自己的網站上創(chuàng)建一個表單蚀狰,該表單發(fā)送一個 POST 請求到同一個 account_delete
端點。現在职员,如果我們讓某人點擊我們表單的提交按鈕(或者通過 JavaScript 來這樣做)麻蹋,郵件提供商提供的登錄賬號就會被刪除掉。當然郵件提供商還不知道表單提交并不是發(fā)生在他們的網站上焊切。
因此如何才能阻止 POST 請求來自別的網站扮授?WTForms 通過在渲染每一個表單的時候生成一個唯一的令牌使得成為可能。生成的令牌會被傳回到服務器专肪,伴隨著 POST 請求的數據刹勃,在表單被接受之前令牌必須接受服務器的驗證。關鍵的是令牌是與存儲在用戶會話(cookies)的一個值有關并且會在一段時間后失效(默認是 30 分鐘)嚎尤。這種方式就能夠保證提交一個有效表單的人就是加載頁面的人(或者至少是使用同一電腦的人)荔仁,而且他們只能在加載頁面 30 分鐘內這樣做。
- 在文檔中 了解更多關于 WTForms 如何生成這些令牌芽死。
- 在 OWASP wiki 中了解 CSRF乏梁。
要開始使用 Flask-WTF 保護 CSRF,我們需要為我們的登錄頁定義一個視圖关贵。
# ourapp/views.py
from flask import render_template, redirect, url_for
from . import app
from .forms import EmailPasswordForm
@app.route('/login', methods=["GET", "POST"])
def login():
form = EmailPasswordForm()
if form.validate_on_submit():
# Check the password and log the user in
# [...]
return redirect(url_for('index'))
return render_template('login.html', form=form)
如果表單已經被提交和驗證的話遇骑,我們可以繼續(xù)登錄的邏輯。如果它沒有被提交的話(例如揖曾,只是一個 GET 請求)落萎,我們就要把表單對象傳遞給我們的模板亥啦,以便它能夠被渲染。下面就是我們使用 CSRF 保護的時候模板的樣子练链。
{# ourapp/templates/login.html #}
{% extends "layout.html" %}
{% endraw %}
<html>
<head>
<title>Login Page</title>
</head>
<body>
<form action="{{ url_for('login') }}" method="post">
<input type="text" name="email" />
<input type="password" name="password" />
{{ form.csrf_token }}
</form>
</body>
</html>
{% raw %}{{ form.csrf_token }}{% endraw %}
渲染了一個隱藏的字段翔脱,該字段包含那些奇特的 CSRF 令牌,并且當 WTForms 驗證表單的時候會尋找這個字段兑宇。我們不用擔心包含處理令牌的邏輯碍侦,WTForms 會主動幫我們去做。好哇隶糕!
使用 CSRF 令牌保護 AJAX 調用
Flask-WTF CSRF 令牌不限于保護表單提交鹃祖。如果你的應用程序要處理其它可能會被偽造的請求(特別是 AJAX 調用)共耍,你也可以在那里添加 CSRF 保護!
- Flask-WTF 文檔中談到了更多地關于 在 AJAX 調用中使用這些 CSRF 令牌纫谅。
自定義驗證
除了由 WTForms 提供的內置的表單驗證器(例如株旷,Required()
再登,Email()
等等),我們能創(chuàng)建我們自己的驗證器晾剖。我們將通過編寫一個 Unique()
驗證器來說明如何創(chuàng)建自己的驗證器锉矢,Unique()
驗證器是用來檢查數據庫并且確保用戶提供的值在數據庫中不存在。這能夠用于確保用戶名或者郵箱地址還沒有使用齿尽。沒有 WTForms 的話沽损,我們可能要在視圖中做這些事情,但是現在我們可以在表單本身做些事情循头。
現在我們來定義一個簡單的注冊表單绵估,其實這個表單和登錄的表單幾乎一樣。只是會在后面給它添加一些自定義的驗證器卡骂。
# ourapp/forms.py
from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email
class EmailPasswordForm(Form):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
現在我們要添加我們的驗證器用來確保它們提供的郵箱地址不存在數據庫中国裳。我們把這個驗證器放在一個新的 util
模塊,util.validators
全跨。
# ourapp/util/validators.py
from wtforms.validators import ValidationError
class Unique(object):
def __init__(self, model, field, message=u'This element already exists.'):
self.model = model
self.field = field
def __call__(self, form, field):
check = self.model.query.filter(self.field == field.data).first()
if check:
raise ValidationError(self.message)
這個驗證器假設我們是使用 SQLAlchemy 來定義我們的模型缝左。WTForms 期待驗證器返回某種可調用的對象(例如,一個可調用的類)浓若。
在 Unique()
的 \\_\\_init\\_\\_
中我們可以指定哪些參數傳入到驗證器中渺杉,在本例中我們要傳入相關的模型(例如,在我們例子中是傳入 User
模型)以及要檢查的字段七嫌。當驗證器被調用的時候少办,如果定義模型的任何實例匹配表單中提交的值,它將會拋出一個 ValidationError
诵原。我們也可以添加一個具有通用默認值的消息英妓,它將會被包含在 ValidationError
中挽放。
現在我們可以修改 EmailPasswordForm
,使用我們自定義的 Unique
驗證器蔓纠。
# ourapp/forms.py
from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired
from .util.validators import Unique
from .models import User
class EmailPasswordForm(Form):
email = StringField('Email', validators=[DataRequired(), Email(),
Unique(
User,
User.email,
message='There is already an account with that email.')])
password = PasswordField('Password', validators=[DataRequired()])
- 我們的驗證器不必須是一個可調用的類辑畦。它也可能是返回可調用或者直接調用的一個工廠模式。WTForms 文檔中有 一些例子腿倚。
渲染表單
WTForms 也能幫助我們?yōu)楸韱武秩境?HTML 表示纯出。WTForms 實現的 Field
字段能夠渲染成該字段的 HTML 表示,所以為了渲染它們敷燎,我們只必須在我們模板中調用表單的字段暂筝。這就像渲染 csrf_token
字段。下面給出了一個登錄模板的示例硬贯,在里面我們使用 WTForms 來渲染我們的字段焕襟。
{# ourapp/templates/login.html #}
{% extends "layout.html" %}
<html>
<head>
<title>Login Page</title>
</head>
<body>
<form action="" method="post">
{{ form.email }}
{{ form.password }}
{{ form.csrf_token }}
</form>
</body>
</html>
我們可以自定義如何渲染字段,通過傳入字段的屬性作為參數到調用中饭豹。
<form action="" method="post">
{{ form.email.label }}: {{ form.email(placeholder='yourname@email.com') }}
<br>
{% raw %}{{ form.password.label }}: {{ form.password }}{% endraw %}
<br>
{% raw %}{{ form.csrf_token }}{% endraw %}
</form>
- 如果我們想要傳入 “class” HTML 屬性鸵赖,我們必須使用 class_='' 因為 “class” 是 Python 中的保留關鍵字。
- WTForms 文檔中有一個 可用字段屬性列表拄衰。
- 你可能注意到我們沒有必須要使用 Jinja 的
|safe
過濾器它褪。這是因為 WTForms 渲染 HTML 安全字符串。 - 更多的內容請參閱 官方文檔翘悉。
摘要
- 表單從安全性的角度來看是很可怕的茫打。
- WTForms(以及 Flask-WTF)使得容易地定義,保護以及渲染你的表單镐确。
- 使用 Flask-WTF 提供的 CSRF 保護可以確保你的表單的安全包吝。
- 你也可以使用 Flask-WTF 來保護你的 AJAX 調用以防止 CSRF 攻擊。
- 定義自定義的表單驗證器可以讓驗證邏輯遠離視圖源葫。
- 使用 WTForms 字段渲染來渲染你的表單的 HTML诗越,在你對你的表單定義做出一些改變的時候,你不必每次都更新它息堂。