フォームは、ユーザーが Web アプリケーションと対話できるようにする基本的な要素です。 Flask 自体はフォームに関して役に立ちませんが、Flask-WTF 拡張機能を使用すると、Flask アプリケーションで人気のある WTForms パッケージを使用できるようになります。このパッケージにより、フォームの定義と送信の処理が簡単になります。
フラスコ-WTF
Flask-WTF (インストール後、GitHub プロジェクト ページ: https://github.com/lepture/flask-wtf) で最初に行うことは、myapp.forms パッケージでフォームを定義することです。
Flask-WTF バージョン 0.9 より前は、Flask-WTF は WTForms フィールドとバリデーター用に独自のパッケージを提供していました。 TextField と PasswordField を wtforms からではなく flask.ext.wtforms からインポートするコードが外部にたくさんあるかもしれません。
Flask-WTF バージョン 0.9 以降では、これらのフィールドとバリデーターを wtforms から直接インポートする必要があります。
ここで定義するフォームはユーザー ログイン フォームです。これを EmailPasswordForm() と呼びます。この同じフォーム クラス (Form) を再利用して、登録フォームなどの他の作業を行うことができます。ここでは、長くて役に立たないフォームを定義するのではなく、Flask-WTF を使用してフォームを定義する方法を紹介するために、非常に一般的に使用されるフォームを選択します。将来的には、特に複雑なフォームが正式なプロジェクトで定義される可能性があります。フィールド名を含むフォームの場合は、フォーム内で明確で一意な名前を使用することをお勧めします。長いフォームの場合は、上記とより一貫性のあるフィールド名を指定する必要があるかもしれないと言わざるを得ません。
ログインフォームではいくつかのことができます。これは、CSRF 脆弱性からアプリケーションを保護し、ユーザー入力を検証し、フォームに定義したフィールドに適切なマークアップをレンダリングします。
CSRF の保護と検証
CSRF は、クロスサイト リクエスト フォージェリの略です。 CSRF 攻撃は、サードパーティがアプリケーションのサーバーへのリクエスト (フォーム送信など) を偽造したときに発生します。脆弱なサーバーは、フォームからのデータが自身の Web サイトから取得されたものであると想定し、それに応じて動作します。
例として、メールプロバイダーがフォームを送信することでアカウントを削除できるようにしているとします。フォームはサーバー上の account_delete エンドポイントに POST リクエストを送信し、フォームが送信されるとログインしているアカウントを削除します。同じ account_delete エンドポイントに POST リクエストを送信するフォームを Web サイト上に作成できます。ここで、誰かがフォーム上の送信ボタンをクリックできるようにすると (または JavaScript を介してクリックすると)、電子メール プロバイダーによって提供されたログイン情報が削除されます。もちろん、電子メールプロバイダーは、Web サイトでフォームの送信が行われていないことをまだ知りません。
それでは、他の Web サイトからの POST リクエストを防ぐにはどうすればよいでしょうか? WTForms は、各フォームをレンダリングするときに一意のトークンを生成することでこれを可能にします。生成されたトークンは、POST リクエストのデータとともにサーバーに返されます。フォームが受け入れられる前に、トークンはサーバーによって検証される必要があります。重要なのは、トークンがユーザーのセッション (Cookie) に保存されている値に関連付けられており、一定の時間 (デフォルトは 30 分) が経過すると期限切れになるということです。こうすることで、有効なフォームを送信した人がページをロードした人と同じ人 (または少なくとも同じコンピュータを使用している同じ人) であることが保証され、ページをロードしてから 30 分以内にのみ送信できるようになります。
Flask-WTF で CSRF の保護を開始するには、ログイン ページのビューを定義する必要があります。
フォームが送信され検証された場合は、ログイン ロジックを続行できます。送信されていない場合 (たとえば、GET リクエストだけ)、レンダリングできるようにフォーム オブジェクトをテンプレートに渡す必要があります。 CSRF 保護を使用する場合のテンプレートは次のようになります。
{% raw %}{{ form.csrf_token }}{% endraw %} は、これらの派手な CSRF トークンを含む隠しフィールドをレンダリングし、WTForms がフォームを検証するときにこのフィールドを探します。トークンを処理するロジックの組み込みについて心配する必要はありません。WTForms が積極的に実行してくれます。万歳!
カスタム検証
WTForms によって提供される組み込みのフォームバリデーター (例: Required()、Email() など) に加えて、独自のバリデーターを作成できます。データベースをチェックし、ユーザーが指定した値がデータベースに存在しないことを確認する Unique() バリデーターを作成することにより、独自のバリデーターを作成する方法を説明します。これを使用すると、ユーザー名または電子メール アドレスがまだ使用されていないことを確認できます。 WTForms がなければ、ビュー内でこれらの作業を行う必要があるかもしれませんが、今ではフォーム自体で作業を行うことができます。
それでは、簡単な登録フォームを定義してみましょう。実際、このフォームはログイン フォームとほぼ同じです。後でカスタム バリデーターをいくつか追加します。
现在我们要添加我们的验证器用来确保它们提供的邮箱地址不存在数据库中。我们把这个验证器放在一个新的 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 也能帮助我们为表单渲染成 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>
处理 OpenID 登录
现实生活中,我们发现有很多人都不知道他们拥有一些公共账号。一部分大牌的网站或服务商都会为他们的会员提供公共账号的认证。举个栗子,如果你有一个 google 账号,其实你就有了一个公共账号,类似的还有 Yahoo, AOL, Flickr 等。
为了方便我们的用户能简单的使用他们的公共账号,我们将把这些公共账号的链接添加到一个列表,这样用户就不用自手工输入了。
我们要把一些提供给用户的公共账号服务商定义到一个列表里面,这个列表就放到配置文件中吧 (fileconfig.py):
CSRF_ENABLED = True SECRET_KEY = 'you-will-never-guess' OPENID_PROVIDERS = [ { 'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id' }, { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' }, { 'name': 'AOL', 'url': 'http://openid.aol.com/<username>' }, { 'name': 'Flickr', 'url': 'http://www.flickr.com/<username>' }, { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }]
接下来就是要在我们的登录视图函数中使用这个列表了:
@app.route('/login', methods = ['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): flash('Login requested for OpenID="' + form.openid.data + '", remember_me=' + str(form.remember_me.data)) return redirect('/index') return render_template('login.html', title = 'Sign In', form = form, providers = app.config['OPENID_PROVIDERS'])
我们从 app.config 中引入了公共账号服务商的配置列表,然后把它作为一个参数通过 render_template 函数引入到模板。
接下来要做的我想你也猜得到,我们需要在登录模板中把这些服务商链接显示出来。
<!-- extend base layout --> {% extends "base.html" %} {% block content %} <script type="text/javascript"> function set_openid(openid, pr) { u = openid.search('<username>') if (u != -1) { // openid requires username user = prompt('Enter your ' + pr + ' username:') openid = openid.substr(0, u) + user } form = document.forms['login']; form.elements['openid'].value = openid } </script> <h1>Sign In</h1> <form action="" method="post" name="login"> {{form.hidden_tag()}} <p> Please enter your OpenID, or select one of the providers below:<br> {{form.openid(size=80)}} {% for error in form.errors.openid %} <span style="color: red;">[{{error}}]</span> {% endfor %}<br> |{% for pr in providers %} <a href="javascript:set_openid('{{pr.url}}', '{{pr.name}}');">{{pr.name}}</a> | {% endfor %} </p> <p>{{form.remember_me}} Remember Me</p> <p><input type="submit" value="Sign In"></p> </form> {% endblock %}
这次的模板添加的东西似乎有点多。一些公共账号需要提供用户名,为了解决这个我们用了点 javascript。当用户点击相关的公共账号链接时,需要用户名的公共账号会提示用户输入用户名, javascript 会把用户名处理成可用的公共账号,最后再插入到 openid 字段的文本框中。
下面这个是在登录页面点击 google 链接后显示的截图: