Flask-WTFによるフラスコフォームバリデーション

フォームバリデーションは、Web アプリケーションのデータ入力において最も重要なコンポーネントの 1 つです。ユーザーは間違いを犯す可能性があり、中には悪意のあるユーザーもいます。入力検証を行うことで、ビジネスロジックに影響を与える不正なデータや、システムに害を及ぼす悪意のある入力からアプリケーションを守ることができます。

検証されていないユーザー入力を処理しようとすると、サーバーがクラッシュしないまでも、予期せぬバグや処理不能のバグが発生する可能性があります。この文脈では、データの検証とは、入力を確認し、それが特定の期待値や基準に合致しているかどうかをチェックすることを意味します。データの検証は、フロントエンドとバックエンドの両方で行うことができます。

このチュートリアルでは、Flask-WTForms 拡張を使用して Flask のフォームでユーザー入力を検証する方法を学びます。

このチュートリアルが終わるころには、以下のようなユーザー登録フォームができ、バリデーション基準もできていることでしょう。

Flask のバージョンは 1.1.2 で、Flask-WTF のバージョンは 0.14.3 を使用する予定です。

セットアップ

必須ではありませんが、仮想環境を作成してフォローすることをお勧めします。

$ mkdir flask-form-validation
$ cd flask-form-validation
$ python3 -m venv .
$ . bin/activate


起動した仮想環境で、次のように入力して、パッケージをインストールします。

$ pip install Flask Flask-WTF


メール検証を使用する場合は、email_validatorパッケージもインストールする必要があることに注意してください (現在のバージョンは 1.1.1 です)。

$ pip3 install email_validator


それでは、必要なファイルを作成していきましょう。まず、基本的な app.py を作成します。単純化するために、Flask アプリ、ルート、フォームを含むことにします。

from flask import Flask, render_template


app = Flask(__name__, template_folder='.')
app.config['SECRET_KEY']='LongAndRandomSecretKey'


Flaskオブジェクトを作成し、 template_folder をカレントフォルダに設定します。そして、Flask オブジェクトを app 変数に代入した。appオブジェクトの設定にSECRET_KEY` を追加した。

SECRET_KEYはデータベース接続やブラウザセッションを暗号化するためによく使われます。WTForms はSECRET_KEY` を CSRF トークンを作成するためのソルトとして使用します。CSRF については、この wiki ページで詳しく説明されています。

もし、あなたのアプリケーションが既に SECRET_KEY を他の目的で使用しているなら、WTForms 用に新しいものを作成したいと思うでしょう。その場合、 WTF_CSRF_SECRET_KEY 設定を設定することができます。

それでは、現在の app.py にフォームを作成し、追加してみましょう。

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField


class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'))
    submit = SubmitField(label=('Submit'))


# ...


シンプルな GreetUserForm クラスは、 StringField を含んでいます。その名前が示すように、このフィールドは文字列の値を期待し、それを返します(必要に応じて、いつでもその入力を他のデータ型に変換することができます)。フィールドの名前は username で、この名前を使ってフォーム要素のデータにアクセスすることになります。

ラベルのパレメータはページ上にレンダリングされるもので、ユーザーはフォーム要素がどのようなデータを取得しているかを理解することができます。また、submit` ボタンもあります。これは、すべてのフィールドがバリデーション基準をパスした場合に、フォームを送信しようとするものです。

これで準備が整いましたので、WTFormsを使ってデータの検証を行いましょう!

Flask-WTForms による Flask フォームバリデーション

まずはフォームを表示し、処理するためのルートを作成しましょう。

# ...


@app.route('/', methods=('GET', 'POST'))
def index():
    form = GreetUserForm()
    if form.validate_on_submit():
        return f'''<h1 Welcome {form.username.data} </h1'''
    return render_template('index.html', form=form)


ルートは GETPOST メソッドを持っています。GETメソッドはフォームを表示し、POSTメソッドは送信時にフォームのデータを処理します。URLパスは/(ルートURL) に設定し、Webアプリのホームページとして表示されるようにします。index.html テンプレートをレンダリングして、form オブジェクトをパラメータとして渡します。

ここで一旦停止して、次の行に注目しましょう: if form.validate_on_submit():. このルールは、「リクエストメソッドがPOSTで、フォームフィールドが有効であれば処理を続行する」というものです。フォームの入力がバリデーション基準をパスした場合、次のページではユーザーの名前とともにシンプルな挨拶メッセージが表示されます。ここでは、入力データにアクセスするためにフィールド名 (username) を使っていることに注意してください。

フォームを表示するには、index.html テンプレートを作成する必要があります。そのファイルを作成し、以下のコードを追加します。

<form action="" method="POST"
<div class="form-row"
<div class="form-group col-md-6"
            {{ form.csrf_token() }}
            <label for="" {{ form.username.label }}</label
            {{ form.username }}
        </div
<div class="form-group"
            {{ form.submit(class="btn btn-primary")}}
        </div
</div
</form


この form オブジェクトを使って、Flask のテンプレートパーサーである Jinja2 に WTform 要素を渡します。

Note: csrf_token は WTForms によって自動的に生成され、ページがレンダリングされるたびに変更されます。これは CSRF 攻撃からサイトを保護するのに役立ちます。デフォルトでは、このフィールドは hidden です。CSRF トークンを含むすべての hidden フィールドをレンダリングするために {{ form.hidden_field() }} を使用することもできますが、これは推奨されません。

では、ターミナルで次のように入力して、Flask アプリを起動してみましょう。

$ FLASK_ENV=development flask run


便利なように、開発中は FLASK_ENV 環境変数を ‘development’ に設定します。これにより、保存を押すたびにアプリがホットリロードされるようになります。Windows では、FLASK アプリを実行する前にターミナル/コンソールで set FLASK_ENV=development を使用する必要があるかもしれません。

localhostに移動すると、このようになります。

入力フィールドに名前を入力し、フォームを送信します。ルートで定義した挨拶文が表示されます。

これは期待通りに動作しています。しかし、もし入力フィールドに何も入力しなかったらどうでしょうか?この場合でもフォームのバリデーションは行われます。

これを防いで、自分の名前を入力したユーザーだけに次のページを表示させるようにしましょう。そのためには、username フィールドに入力データがあることを確認する必要があります。

WTFormsの組み込みの検証メソッドの1つである DataRequired()wtforms.validators からインポートして、 username フィールドに渡します。

# ...
from wtforms.validators import ValidationError, DataRequired


class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
                           validators=[DataRequired()])
    submit = SubmitField(label=('Submit'))


# ...


パラメータ validators をリストとして渡していることに注意してください。これは、各フィールドに複数のバリデータを指定できることを示しています。

ここでは DataRequired() を使用しているので、入力データがない場合は username フィールドは検証されません。

実際、右クリックしてフォームの要素を調べてみると、WTForms が自動的に入力フィールドに required 属性を追加していることがわかります。

このようにすることで、WTFormsはフォームフィールドに基本的なフロントエンドのバリデーションを追加しています。cURLやPostmanのようなツールを使ってフォームを投稿しようとしても、usernameフィールドがなければフォームを投稿することはできないでしょう。

ここで、少なくとも5文字以上の名前しか許可しないような新しいバリデーションルールを設定したいとしましょう。この場合、Length() バリデータに min パラメータをつければよいでしょう。

# ...
from wtforms.validators import ValidationError, DataRequired, Length


class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'), 
        validators=[DataRequired(), Length(min=5)])
    submit = SubmitField(label=('Submit'))


# ...


5 文字未満の入力データでフォームを送信しようとすると、バリデーション基準を満たすことができず、送信は失敗します。

送信ボタンをクリックしても、無効なデータに対しては何もしませんし、ユーザーに対してエラーも表示しません。エラーメッセージを表示して、何が起こっているのか、どのように修正すればいいのかをユーザーに理解してもらう必要があります。

index.htmlテンプレートの}` のすぐ下に、以下の Jinja2 for-loop を追加して、エラーを表示するようにします。

 {% for field, errors in form.errors.items() %}
    <small class="form-text text-muted"
        {{ ', '.join(errors) }}
    </small
{% endfor %}


これで、フォームがきれいなバリデーションエラーを表示できるようになりました。

何らかの理由でフィールドデータの最大長を制限する必要がある場合、 Length() バリデータに max パラメータを渡すことで実現できます。また、オプションの message パラメータにカスタムエラー文字列を渡すことで、エラーメッセージをカスタマイズすることもできます。

それでは、username フィールドを更新してみましょう。

# ...


class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
        validators=[DataRequired(), 
        Length(min=5, max=64, message='Name length must be between %(min)d and %(max)dcharacters') ])
    submit = SubmitField(label=('Submit'))


# ...


ユーザー登録フォームでWTFormsフィールドとバリデーターを増やす

現在のフォームにはフィールドが1つしかなく、なんだか味気ないですね。WTForms は広範なフォーム検証条件とさまざまなフォームフィールドを提供しますから、それを利用して実用的なものを作ってみましょう。

ユーザー登録フォームを作成し、WTForms の組み込みバリデーターを使用することにします。

ユーザーに確実に入力してもらいたいフィールドには DataRequired() バリデータを使用します。フィールドの長さの最小値と最大値を Length() バリデータでチェックし、メールアドレスを Email() バリデータで検証し、2 つのフィールドが同じデータを含んでいるかどうかを EqualTo() バリデータでチェックしましょう。

GreetUserForm` クラスを削除し、コードの冒頭を新しいフォームに置き換えます。

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, 
    SubmitField
from wtforms.validators import ValidationError, DataRequired, 
    Email, EqualTo, Length


class CreateUserForm(FlaskForm):
    username = StringField(label=('Username'), 
        validators=[DataRequired(), 
        Length(max=64)])
    email = StringField(label=('Email'), 
        validators=[DataRequired(), 
        Email(), 
        Length(max=120)])
    password = PasswordField(label=('Password'), 
        validators=[DataRequired(), 
        Length(min=8, message='Password should be at least %(min)d characters long')])
    confirm_password = PasswordField(
        label=('Confirm Password'), 
        validators=[DataRequired(message='*Required'),
        EqualTo('password', message='Both password fields must be equal!')])


receive_emails = BooleanField(label=('Receive merketting emails.'))


submit = SubmitField(label=('Submit'))


# ...


このフォームには4つのフィールドがあります。最後の1つは通常の送信ボタンです。ユーザー名やメールアドレスなどの文字列を入力するには、 StringField を使用します。一方、 PasswordField はフロントエンドでパスワードのテキストを隠します。BooleanField` は、True (Checked) か False (Unchecked) のどちらかの値のみを含むので、フロントエンドではチェックボックスとしてレンダリングされます。

新しいフォームフィールドをレンダリングするために、 index.html テンプレートを変更する必要があります。

<div class="container"
<h2Registration Form</h2
    {% for field, errors in form.errors.items() %}
    {{ ', '.join(errors) }}
    {% endfor %}
    <form action="" class="form-horizontal" method="POST"
        {{ form.csrf_token() }}
        <div class="form-group"
            {{ form.username.label }}
            {{ form.username(class="form-control") }}
        </div
<div class="form-group"
            {{ form.email.label }}
            {{ form.email(class="form-control") }}
        </div
<div class="form-group"
            {{ form.password.label }}
            {{ form.password(class="form-control") }}
        </div
<div class="form-group"
            {{ form.confirm_password.label }}
            {{ form.confirm_password(class="form-control") }}
        </div
<div class="form-group"
            {{ form.receive_emails.label }}
        </div
<div class="form-group"
            {{ form.submit(class="btn btn-primary")}}
        </div
</form
</div


このように、フォームフィールドは正しくレンダリングされました。

注意: あなたのウェブサイトが複数の異なるフォームを持つことになる場合、各フォームフィールドを一つずつタイプする代わりに、Jinja2 マクロを使用するとよいでしょう。マクロの使用はこの記事の範囲外ですが、フォームの作成プロセスを大幅にスピードアップさせることができます。

独自のカスタムバリデータを作成する

ほとんどの Web サイトでは、ユーザーネームに特定の文字を使用することはできません。それはセキュリティのためであったり、美化のためであったりします。WTForms はデフォルトではそのようなロジックを持ちませんが、私たち自身で定義することができます。

WTFormsでは、UserRegistrationFormクラスにバリデーションメソッドを追加することで、カスタムバリデータを追加することができます。では、submitボタンのすぐ下に validate_username() メソッドを追加して、カスタムバリデーションをフォームに実装してみましょう。

# ...


class UserRegistrationForm(FlaskForm):
    # ...
    submit = SubmitField(label=('Submit'))


def validate_username(self, username):
        excluded_chars = " *?!'^+%&amp;/()=}][{$#"
        for char in self.username.data:
            if char in excluded_chars:
                raise ValidationError(
                    f"Character {char} is not allowed in username.")

# ...


好きなだけ多くのバリデーションメソッドを追加することができます。WTFormsは一度定義すれば、自動的にバリデーションメソッドを実行します。

ValidationErrorクラスは、独自のバリデーションメッセージを定義する便利な方法を提供します。このクラスを使用する前に、wtforms.validators` からインポートする必要があることに注意してください。

この新しい方法をテストするために、除外文字である ‘%’ を含む username フィールドを除くすべてのフィールドに適切なデータを入力してみましょう。

見てわかるように、このカスタム検証メソッドは完璧に動作し、きれいな検証エラーを表示します。こうすることで、ユーザーエクスペリエンスを大幅に向上させることができます。

外部ライブラリやデータベース、APIを利用してWTFormsと組み合わせ、入力されたデータの検証を行うことができます。form.some_field.data }}` を取得し、データベースに書き込んだり、データベースから問い合わせたりしたい場合、WTForms バリデータを使用して、保存しても安全であることを確認します。

注:HTMLコードのほとんどは、このチュートリアルに直接関係しないので、除外しました。完全なコードはこのGitHubリポジトリで公開されますので、チェックアウトしたい場合はご覧ください。

結論

データの検証は Flask の Web アプリケーションで最も重要な部分の一つです。Flask-WTforms は、フォームデータを扱うための非常に強力かつ習得しやすい方法を提供します。

これで Flask-WTF でのデータ検証の基本がわかったと思いますので、あとは独自の検証ロジックを適用したり、独自の方法を実装したりして、セキュリティとユーザーエクスペリエンスの両方を向上させましょう。

タイトルとURLをコピーしました