
# 處理表單
表單是允許用戶跟你的web應用交互的基本元素。Flask自己不會幫你處理表單,但Flask-WTF插件允許用戶在Flask應用中使用膾炙人口的WTForms包。這個包使得定義表單和處理表單功能變得輕松。
## Flask-WTF
你首要做的事(當然是在安裝Flask-WTF之后),就是在`myapp.forms`包下定義一個表單類(form)。
_myapp/forms.py_
```python
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()])
```
> **注意**
> 直到0.9版,Flask-WTF為WTForms的fields和validators提供自己的包裝。你可能見過許多代碼直接從`flask_wtforms`而不是`wtforms`中直接導入`TextField`,`PasswordField`等等。
> 而從0.9版之后,我們得直接從`wtforms`中導入它們。
這個表單將用于用戶注冊表單。我們可以稱之為`SignInForm()`,但是通過保持抽象,我們可以在別的地方重用它,比如作為登錄表單。如果我們針對特定功能定義表單,最終就會得到許多相似卻無法重用的表單。基于表單中包含的域 - 那些使得表單與眾不同的元素 - 進行命名,顯然會清晰很多。當然,有時候你會有復雜的,只在一個地方用到的表單,你再給它起個獨一無二的名字也不遲。
這個表單可以幫我們做一些事情。它可以保護我們的應用免遭CSRF傷害,驗證用戶輸入,為我們定義的域渲染適當的標記。
### CSRF保護和驗證
CSRF全稱是cross site request forgery,跨站請求偽造。CSRF通過第三方偽造表單數據,post到應用服務器上。受害服務器以為這些數據來自于它自己的網站,于是大意地中招了。
舉個例子,假設你的郵件服務商允許你通過提交一個表單來注銷賬戶。這個表單發送一個POST請求到他們服務器的`account_delete`頁面,并且用戶已經登錄,就可以注銷賬戶。你可以在你自己的網站中創建一個會發送到同一個`account_delete`頁面的表單。現在,假如有個倒霉蛋點擊了你的表單的'submit'(或者在他們加載你的頁面的時候通過Javascript做到這一點),同時他們又登錄了郵件賬號,那么他們的賬戶就會被注銷。除非你的郵件服務商知道不能假定提交過來的請求都是來自于自己的頁面。
所以我們怎樣判斷一個POST請求是否來自我們自己的表單呢?WTForms在渲染每個表單時生成一個獨一無二的token,使得這一切變得可能。那個token將在POST請求中隨表單數據一起傳遞,并且會在表單被接受之前進行驗證。關鍵在于token的值取決于儲存在用戶的會話(cookies)中的一個值,而且會在一定時間之后過時(默認30分鐘)。這樣只有登錄了頁面的人(或至少是在那個設備之后的人)才能提交一個有效的表單,而且僅僅是在登錄頁面30分鐘之內才能這么做。
> **參見**
> * 這里是關于WTForms是怎么生成token的文檔: http://wtforms.simplecodes.com/docs/1.0.1/ext.html#module-wtforms.ext.csrf.session
> * 這里有關于CSRF更多的信息: https://www.owasp.org/index.php/CSRF
為了開始使用Flask-WTF做CSRF防護,我們得先給我們的登錄頁定義一個視圖。
myapp/views.py
```python
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)
```
我們從`forms`包中導入form對象,并于視圖內實例化。然后運行`form.validate_on_submit()`。如果表單已經submit了(比如通過HTTP方法PUT或POST),這個函數返回`True`并且用定義在*forms.py*中的驗證函數來驗證表單。
> **參見**
> `validate_on_submit()`的文檔和源碼在此:
> * http://flask-wtf.readthedocs.org/en/latest/api.html#flask_wtf.Form.validate_on_submit
> * https://github.com/ajford/flask-wtf/blob/v0.8.4/flask_wtf/form.py#L120
如果一個表單已經提交并且通過驗證,我們可以開始處理登錄邏輯的部分了。如果它還沒有提交(比如,它只是一個GET請求),我們需要傳遞這個表單對象給模板來進行渲染。下面展示如何在模板中使用CSRF防護。
myapp/templates/login.html
```
{% extends "layout.html" %}
<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>
```
`{{ form.csrf_token }}`將渲染一個隱藏的包括防范CSRF的特殊token的域,而WTForms會在驗證表單時查找這個域。我們不用操心添加的任何特殊的驗證token正確性的邏輯。萬歲!
#### 使用CSRFtoken來保護AJAX調用
Flask-WTF的CSRF token不僅限于保護表單提交。如果你的應用需要接受其他可能被偽造的請求(特別是AJAX調用),你也可以給它們添加CSRF保護!想了解更多信息,請查看Flask-WTF的文檔:https://flask-wtf.readthedocs.org/en/latest/csrf.html#ajax
### 自定義驗證函數
除了WTForms提供的內置表單驗證函數(比如`Required()`,`Email()`等等),你可以創建自己的驗證函數。通過創建一個可用于檢查數據庫并確保用戶提供的值未曾存在的`Unique()`驗證函數,我將展示這一點。這個函數可以確保一個用戶名或郵件地址未被使用。如果沒有WTForms,我們不得不在視圖中完成這些檢查,但現在我們可以抽象出來作為form類的一部分。
_myapp/forms.py_
```python
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`。
_myapp/util/validators.py_
```
from wtforms.validators import ValidationError
class Unique(object):
def __init__(self, model, field, message=u'該內容已經存在。'):
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要求驗證函數返回可調用的(callable)類型(比如一個可調用的類)。
在*\_\_init\_\_.py*中,我們可以指定哪些參數應該傳遞給驗證函數。在這個例子中我們需要檢查相關的模型(比如`User`模型)和域。當驗證函數被調用時,如果表單提交的值跟定義的模型的某個實例重復了,它會拋出一個`ValidationError`。我們也提供一個帶默認值的信息參數,作為`ValidationError`的一部分。
現在我們給`EmailPasswordForm`添加`Unique`驗證器。
_myapp/forms.py_
```
from flask_wtf import Form
from wtforms import StringField, PasswordField,
from wtforms.validators import DataRequired, Email
from .util.validators import Unique
from .models import User
class EmailPasswordForm(Form):
email = StringField('Email', validators=[DataRequired(), Email(), Unique(User, User.email, message='該郵箱已被用于注冊'])
password = PasswordField('Password', validators=[DataRequired()])
```
> **注意**
> 你的驗證函數不一定需要是可調用的類。它也可以是一個返回可調用對象的工廠類或者可調用對象。看這里的一些例子:
> http://wtforms.simplecodes.com/docs/0.6.2/validators.html#custom-validators
### 渲染表單
WTForms也可以幫助我們給我們只需要表單渲染HTML。WTForms實現的`Field`類能根據域的形式渲染對應的HTML,所以我們只需要在模板中調用它們。就像是渲染`csrf_token`域一樣。下面是當我們使用WTForms來渲染我們的其他域時,login模板大概的樣子。
myapp/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>
```
通過傳遞域的性質(properties)作為調用域的參數,我們可以自定義域的渲染形式。下面我們添加一個`placeholder=`性質給email域:
```
<form action="" method="POST">
{{ form.email.label }}: {{ form.email(placeholder='yourname@email.com') }}<br>
{{ form.password.label }}: {{ form.password }}<br>
{{ form.csrf_token }}
</form>
```
> **注意**
> 如果我們想要傳遞HTML屬性“class”, 我們得使用`class_=''`,因為“class”是Python的保留關鍵字。
> **參見**
> 這個文檔列出了所有可用的域性質:
> http://wtforms.simplecodes.com/docs/1.0.4/fields.html#wtforms.fields.Field.name
> **注意**
> 你大概注意到了我們不需要使用Jinja的`|safe`過濾器。這是因為WTForms自己會處理掉HTML轉義的問題。在這里了解更多信息:
> http://pythonhosted.org/Flask-WTF/#using-the-safe-filter
## 總結
* 表單可能會是安全上的阿喀琉斯之踵。
* WTForms(以及Flask-WTF)使得定義,保護和渲染你的表單更加輕松。
* 使用Flask-WTF提供的CSRF防范來保護你的表單。
* 你也可以使用Flask-WTF來防止AJAX調用遭到CSRF攻擊。
* 定義自定義的表單驗證函數,避免在視圖函數中寫入驗證邏輯。
* 使用WTForms的域渲染功能來渲染你的表單的HTML,這樣每次修改表單的定義時,你不需要更新模板。