フォームはWEBアプリケーションでユーザーの入力を扱うための基本的な機能です。 Flask自体が直接フォームを処理するわけではありませんが、Flask-WTFという拡張モジュールでWTFormsという有名なパッケージを利用することが出来ます。 WTFormsはフォームを定義したり送信されたフォームの処理を簡単するためのパッケージです。
Flask-WTFをインストールした後、まず初めにmyapp.forms
パッケージにフォームを定義します。
# ourapp/forms.py
from flask.ext.wtf import Form
from wtforms.fields import TextField, PasswordField
from wtforms.validators import Required, Email
class EmailPasswordForm(Form):
email = TextField('Email', validators=[Required(), Email()])
password = PasswordField('Password', validators=[Required()])
注記
Flask-WTFのバージョン0.9まではWTFormsのフィールドやバリデーターをラップしていました。
ですのでTextField
やPasswordField
を利用する際はwtforms
の代わりにflask.ext.wtforms
からインポートする必要がありました。
0.9以降は例のようにwtforms
からインポートしてください。
先ほど定義したのはサインインフォームです。
SignInForm()
という名前を付けたくなるかもしれませんが、このフォームを別の用途で再利用出来る様に、ここでは抽象的な名前にしておきます。
目的に応じてフォームの名前を付けると、同じ内容のフォームを複数定義することになり、あまり良くありません。
フォームに含まれるフィールドに基づいてユニークな名前を付けたほうがより簡潔になります。
もちろん、1箇所でしか利用しないフォームは目的に応じた名前を付けても良いでしょう。
このサインインフォームは幾つかの機能を持っています。 CSRF対策を行ったり、入力値の検証を行い適切にレンダリングすることも可能です。
CSRFとはクロスサイトリクエストフォージェリの略です。 CSRF攻撃は第3者がフォーム送信などのリクエストを強要させることです。 脆弱なサーバーはそのリクエストをフォームから送信されたデータとして処理してしまいます。
例えば、メールアカウントを削除するフォームがあるとします。
ログイン済みのユーザーがこのフォームからaccount_delete
というエンドポイントに対してPOSTリクエストを送信するとアカウントが削除されます。
これと同じエンドポイントに対してPOSTリクエストを行うフォームは第三者でも作成することが出来ます。
そしてログインを行った誰かが偽装された「送信」ボタンを押したり、JavaScriptを実行するとアカウントを削除されてしまうのです。
これを防ぐには、送信されたPOSTリクエストが自分たちの正しいフォームから送信されたリクエストであることを知る必要があります。
それではどの様にして、正しいフォームから送信されたことを判別すれば良いのでしょうか? WTFormsはフォームをレンダリングする際にユニークなトークンを生成することが出来ます。 POSTリクエストがサーバーに送られてくると、フォームデータを処理する前にこのトークンを検証します。 重要なのはこのトークンはユーザーのセッション(Cookie)に紐付けられており、一定の時間(30分程度)で有効期限が切れるということです。 これにより正しいフォームを送信できる人物は、同じパソコンを利用して一定時間以内にページを読み込んだユーザーのみに限られます。
注記
- WTFormsが生成するトークンについての詳細はこちらのドキュメントを参照してください。
- CSRFについてもっと学ぶにはOWASP wikiを読んでください。
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():
# パスワードのチェックとログイン処理
# [...]
return redirect(url_for('index'))
return render_template('login.html', form=form)
さきほど定義したforms
パッケージからフォームインポートして呼び出します。
この時、form.validate_on_submit()
を実行します。
この関数はHTTPメソッドがPOSTかPUTであり、forms.pyで定義した検証が通ればTrue
を返します。
注記
正しく検証が通ればログイン処理を継続することが出来ます。 POSTリクエストでなくGETリクエストの場合はテンプレートに記述したフォームレンダリングします。 以下は、CSRF対策を行ったフォームテンプレートです。
{# ourapp/templates/login.html #}
{% extends "layout.html" %}
<html>
<head>
<title>ログインページ</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トークンを含むhiddenフィールドをレンダリングします。
このトークンの正当性はform.validate_on_submit()
で検証しますので、これ以上のことは行う必要はありません、わーい。
Flask-WTFのCSRFトークンはフォーム送信以外のリクエストの保護も行います。 アプリケーションがAJAXの様なリクエストを行う場合でもCSRF対策を行うことが出来ます。
注記
詳しくはFlask-WTFのドキュメントを参照してください。
WTFormsが提供する組み込みのバリデーター(Required()
やEmail()
など)だけでなく、バリデーターを自作することも可能です。
この節では、データーベースをチェックして同一の値が存在するかどうかを確認するUnique()
バリデーターを作成する例を示します。
これはユーザー名やメールアドレスをチェックして既に利用されているかどうかを確認することが出来ます。
WTFormsを使用しなければビューの中でチェックしていたでしょう。WTFormsを利用することでバリデーター処理をフォームの中に隠蔽することが出来ます。
それでは単純なサインアップフォームを定義してみましょう。
# ourapp/forms.py
from flask.ext.wtforms import Form
from wtforms import TextField, PasswordField, Required, Email
class EmailPasswordForm(Form):
email = TextField('Email', validators=[Required(), Email()])
password = PasswordField('Password', validators=[Required()])
そして既にデーターベースにこのメールアドレスが存在するかどうかを確認するバリデーターを実装します。
この新しいバリデーターは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はバリデーターが呼び出し可能オブジェクトであることを期待します。
*__init__*ではバリデーターに渡す引き数を指定できます。
今回の例では関連するモデル(User
モデル)とチェックを行うフィールドを渡します。
バリデーターが呼び出されフォームから送信された値がデータベースに存在した場合、ValidationError
を投げます。
この時表示するメッセージも指定するとこが可能です。
それではこのUnique
バリデーターを使う様にEmailPasswordForm
を修正してみましょう。
# ourapp/forms.py
from flask.ext.wtforms import Form
from wtforms import TextField, PasswordField, Required, Email
from .util.validators import Unique
from .models import User
class EmailPasswordForm(Form):
email = TextField('Email', validators=[Required(), Email(),
Unique(
User,
User.email,
message='There is already an account with that email.'])
password = PasswordField('Password', validators=[Required()])
注記
バリデーターは呼び出し可能なクラスである必要はありません。 呼び出し可能オブジェクトまたは普通の関数でも構いません。 WTFormsのドキュメントに幾つかの例が載っています。
WTFormsはフォームをHTMLとしてレンダリングする機能も持っています。
WTFormsのField
クラスはテンプレートの中で呼び出すだけでHTMLをレンダリングします。
csrf_token
フィールドと同じような感じです。
以下にWTFormsを利用してログインフォームをレンダリングする例を示します。
{# ourapp/templates/login.html #}
{% extends "layout.html" %}
<html>
<head>
<title>ログインページ</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>
{{ form.password.label }}: {{ form.password }}
<br>
{{ form.csrf_token }}
</form>
注記
HTML属性の「class」を指定したい時は、class_=''
という引き数を指定する必要があります。
なぜなら「class」というキーワードはPythonで予約されているからです。
注記
WTFormsのドキュメントに利用可能なフィールドプロパティの一覧があります。
注記
知っていると思いますがここでJinjaの|safe
を使う必要はありません。
WTFormsは安全なHTML文字列をレンダリングしてくれます。
詳細はドキュメントを参照してください。
- フォームはセキュリティ上恐ろしいことが出来てしまいます。
- WTForms(Flask-WTF)は安全なフォームを簡単に定義することができます。
- Flask-WTFのCSRF保護機能を利用して安全なフォームを作成してください。
- Flask-WTFを利用して、AJAX呼び出しのCSRF対策行うことができます。
- バリデーションのロジックをビューの外に記述したい場合はカスタムバリデーターを定義します。
- WTFormsのフィールドに基づいてフォームのHTMLがレンダリングされますので、HTMLを直接記述する必要はありません。