チュートリアルに関する3本目の記事です。
参考サイト: Flask – Tutorial(公式ドキュメント)
- チュートリアル
- クイックスタート
4. BlueprintsとViews
view関数は、アプリケーションへのリクエストに対し、レスポンスを返す関数です(Webアプリケーションの場合レスポンスは基本的にHTMLであり、ブラウザに表示されるデータでありviewという呼称と適合していることが理解できると思います)。FlaskはリクエストのURLとそれを処理するview関数を照合するためにパターンマッチを行います。view関数はレスポンスとなるデータを返すとともに、リクエストURLを他のURLに誘導(リダイレクト)することができ、関数の名称や引数に基づいてview関数からURLを生成することもできます(url_for
関数を利用するなど)。
4-1. Blueprintの作成
Blueprint
は、関連するview関数とそのコードをまとめる1つの方法です。view関数や他のコードを直接アプリケーション(Flaskインスタンス。このアプリケーションの場合アプリケーションファクトリー)に登録するのではなく、blueprintインスタンスにまず登録します。そしてblueprintインスタンスは、アプリケーションファクトリー(create_app
関数)に登録されます。
※「blueprint」は「設計図」を意味する言葉です。blueprintを利用することでアプリケーションの各役割ごと(ここでは認証とブログ)にフォルダやコードを構造的に管理できます。
Flaskrは、2つのblueprintインスタンスを持たせ、1つは認証用、もう一つはブログ用です。それぞれのblueprintインスタンスのコードは別々のモジュールに格納されます。ブログに投稿するにはログインしユーザ認証を済ませておく必要があるので、まずは認証用のblueprintインスタンスから記述しています。
# flaskr/auth.py
import functools
from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash
from flaskr.db import get_db
bp = Blueprint('auth', __name__, url_prefix='/auth')
ここではauth
と名付けられたBlueprint
が作られています。アプリケーションオブジェクトのように、定義された場所を把握する必要がありますが、第2引数で__name__
を指定することで格納場所がわかるようにしています。url_prefix
は、blueprintに関連するあらゆるURLにつく接頭辞です(今回の場合はhttp://domain/auth/<url>
)。auth
ファイルをインポートし、app.register_blueprint
を使ってアプリケーションファクトリーにblueprintを登録します。
# flaskr/__init__.db
def create_app():
app = ...
# existing code omitted
from . import auth
app.register_blueprint(auth.bp)
return app
認証用blueprintは、新しいユーザを登録し、ログイン、ログアウトを行うためのview関数をもちます。
functools
…高階関数用の標準モジュール。werkzeug
…WSGIのユーティリティーモジュールwerkzeug.security
…セキュリティ用のwerkzeugのモジュールgenerate_password_hash()
…指定したメソッドでハッシュ値を作るcheck_password_hash
…与えられたパスワードが適切なものかチェックする
4-2. view関数: Register
ユーザがURL/auth/register
にアクセスすると、register
view関数は入力用フォームを持ったHTMLを返します。そのフォームが入力してサーバに返されると、入力内容が検証され、エラーメッセージを持つフォーム画面のHTMLを再送するか、新しいユーザを作成しログイン画面に遷移します。
ここではview関数のみを記述し、HTMLフォームを生成するテンプレートについては「5.テンプレート」で触れます。
# flaskr/auth.py
@bp.route('/register', methods=('GET', 'POST')) # 1.register関数とURL"/register"との関連付け
def register():
if request.method == 'POST': # 2.ユーザがフォームの値を返した場合
username = request.form['username'] # 3.ユーザが入力したusernameとpasswordの値
password = request.form['password']
db = get_db()
error = None
if not username: # 4.usernameが空でないか
error = 'Username is required.'
elif not password: # 4.passwordが空でないか
error = 'Password is required.'
elif db.execute( # 5.usernameが既に登録されていないか
'SELECT id FROM user WHERE username = ?', (username,)
).fetchone() is not None:
error = 'User {} is already registered.'.format(username)
if error is None: # 6.インプットの値が妥当であった場合
db.execute(
'INSERT INTO user (username, password) VALUES (?, ?)',
(username, generate_password_hash(password))
)
db.commit()
return redirect(url_for('auth.login')) # 7.ログインページにリダイレクト
flash(error) # 8.インプットの値が妥当でない場合
return render_template('auth/register.html') # 9.ユーザが最初にページを訪れた場合 or エラーで再度画面を表示する場合
register
view関数の内容は以下になります。
- @
bp.route()
は、URL/register
とregister
view関数を結びつけます。FlaskがURL/auth/register
でリクエストをもらうと、register
view関数が呼び出され、レスポンスを返します。 - ユーザがフォームの値を返すと、
request.method
はPOST
になります。この場合インプット内容の検証を行います。 request.form
は、key-value形式のdictの特別な型です。ユーザは、username
とpassword
を入力します。username
とpassword
が空でないか確認します。username
が既に登録されていないかデータベースに問い合わせ、返ってきた値をチェックすることでユーザデータを検証します。db.execute
では、SQLクエリ内で?
を使っていますが、ここにユーザのインプット値が入ります。また、タプルも取ることもできます。データベースライブラリは値をエスケープ処理するため、SQLインジェクトションアタックに対する脆弱性対策をしています。
fetchone()
はクエリから1つのレコードを返します。クエリにマッチする値がない場合、None
が戻り値となります。後で使われるfetchall()
は、全ての戻り値を格納したリストを返します。- インプットが有効であった場合新しいユーザデータをデータベースに挿入します。セキュリティの観点から、パスワードはデータベースに直接登録すべきではなく。パスワードを安全にハッシュ化する
generate_password_hash()
を使い、パスワードのハッシュ値を保存します。このクエリはデータを変更するため、その変更を反映させるためにdb.commit()
が続いて呼び出されています。 - ユーザデータを保存した後、そのユーザはログインページへとリダイレクトされます。
url_for()
は、ログインview関数の名前に基づくURLを生成します。これは後でこのURLに紐づくコードを変えずに、URLだけを変えることができるため、直接URLを指定する方法よりも便利な方法です。redirect()
で、生成されたURLにリダイレクトされます。 - インプットが有効ではない場合、ユーザにはエラーメッセージが表示されます。
flash()
は、テンプレートを読み込むときに取り出されるメッセージを保存する関数です。 - ユーザが最初にURL
/auth/register
にアクセスする場合かまたはエラーが発生した場合、登録フォームがあるページが表示される必要があります。render_template()
は、次のステップで扱うHTMLを含むテンプレートを読み込みます。
4-3. view関数:Login
この関数は、register
view関数と同じようなパターンになります。
# flaskr/auth.py
@bp.route('/login', methods=('GET', 'POST'))
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
user = db.execute( # 1.userの照合
'SELECT * FROM user WHERE username = ?', (username,)
).fetchone()
if user is None:
error = 'Incorrect username.'
elif not check_password_hash(user['password'], password): # 2.パスワードをハッシュ値の比較で照合
error = 'Incorrect password.'
if error is None:
session.clear()
session['user_id'] = user['id'] # 3.sessionにユーザのidを保存
return redirect(url_for('index'))
flash(error)
return render_template('auth/login.html')
register
view関数との違い
- ユーザはまずデータベースに照会され、後での使用のために変数に格納されます。
check_password_hash()
は、保存された時と同じように提示されたパスワードをハッシュ化し、安全に保存されたパスワードと照合されます。ハッシュ値が一致した場合、パスワードが認証されたことになります。session
は、リクエストをまたいでデータを保存するdict型のデータです。ユーザ情報が有効であると確認できた場合、新しいsessionにユーザのid
が保存されます。このデータはブラウザに送られるクッキーの中に保存され、続くリクエストで送り返されます。Flaskは、それが不正に書き換えられないよう、そのデータに安全に署名を行います。
今、ユーザのid
はsession
の中に保存されたので、連続するリクエストの中で利用できるようになります。ユーザがログインしている場合、それぞれのリクエストの最初でこれらの情報は読み取られ、他のview関数の中で利用できます。
# flaskr/auth.py
@bp.before_app_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = get_db().execute(
'SELECT * FROM user WHERE id = ?', (user_id,)
).fetchone()
bp.before_app_request
は、view関数の前に走らせる関数を登録するデコレータで、URLに関係なく実行されます。load_logged_in_user
はユーザのid
が保存されているかチェックし、データベースからユーザデータを取得し、g.user
に保存します。これはそのリクエストの間、保持されます。ユーザIDがセッションにない場合、またはそのユーザIDがデータベースに存在しない場合、g.user
はNone
となります。
4-4. view関数: Logout
ログアウトするには、session
からユーザIDを削除します。そうすることで、load_logged_in_user
関数は続くリクエストでユーザの照合をしなくなります。
# flaskr/auth.py
@bp.route('/logout')
def logout():
session.clear()
return redirect(url_for('index'))
4-5. 他のview関数での認証要求
投稿の作成、編集、削除をするには、ユーザがログインしている必要があります。デコレータはそれぞれのview関数でログインを確認するために利用されます。
# flaskr/auth.py
def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('auth.login'))
return view(**kwargs)
return wrapped_view
このデコレータはユーザがリクエストしたURLのview関数をラップし、新たなview関数を返します。この新たなview関数は、ユーザ情報を存在するか(ログインしているか)を確認し、していない場合にはログインページへとリダイレクトさせます。ユーザ情報がある場合(ログインしている場合)、オリジナル関数が呼び出されて、通常通りのプログラムが続きます。このデコレータはblogのview関数を記述するときに利用されます。
- @
functools.wraps()
…update_wrapper()
を呼び出すための便宜的な関数update_wrapper()
…wrapper関数をwrapped関数に見せかけるようにアップデートするメソッド__doc__
属性などはデコレートした側(上段の@がついている方)のデータが格納されるが、この関数を呼び出すことでデコレートされた側(@がつけられた方)のデータに置き換わる
4-6. エンドポイントとURL
url_for
関数は、名前や引数に応じたview関数に適合するURLを生成します。view関数に関連する名称はエンドポイントと呼ばれ、デフォルトではiew関数の名前と一致します。
例えば、チュートリアルの最初で試したhello()
view関数はhello
という名称を持つため、url_for('hello')
で紐づけることができます。後に出てきますが、引数を取る場合はurl_for('hello', who='World')
とすることで紐づけることができます。
blueprintを使う場合、そのblueprintの名称は関数の名称の前に追記されます。上述のlogin
関数のエンドポイントはauth
blueprintに追加されているので、auth.login
となります。