Blow Up by Black Swan

Python-Flask で作る Web アプリケーション(チュートリアル編3)

チュートリアルに関する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にアクセスすると、registerview関数は入力用フォームを持った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 エラーで再度画面を表示する場合

registerview関数の内容は以下になります。

  1. @bp.route()は、URL/registerregisterview関数を結びつけます。FlaskがURL/auth/registerでリクエストをもらうと、registerview関数が呼び出され、レスポンスを返します。
  2. ユーザがフォームの値を返すと、request.methodPOSTになります。この場合インプット内容の検証を行います。
  3. request.formは、key-value形式のdictの特別な型です。ユーザは、usernamepasswordを入力します。
  4. usernamepasswordが空でないか確認します。
  5. usernameが既に登録されていないかデータベースに問い合わせ、返ってきた値をチェックすることでユーザデータを検証します。db.executeでは、SQLクエリ内で?を使っていますが、ここにユーザのインプット値が入ります。また、タプルも取ることもできます。データベースライブラリは値をエスケープ処理するため、SQLインジェクトションアタックに対する脆弱性対策をしています。
    fetchone()はクエリから1つのレコードを返します。クエリにマッチする値がない場合、Noneが戻り値となります。後で使われるfetchall()は、全ての戻り値を格納したリストを返します。
  6. インプットが有効であった場合新しいユーザデータをデータベースに挿入します。セキュリティの観点から、パスワードはデータベースに直接登録すべきではなく。パスワードを安全にハッシュ化するgenerate_password_hash()を使い、パスワードのハッシュ値を保存します。このクエリはデータを変更するため、その変更を反映させるためにdb.commit()が続いて呼び出されています。
  7. ユーザデータを保存した後、そのユーザはログインページへとリダイレクトされます。url_for()は、ログインview関数の名前に基づくURLを生成します。これは後でこのURLに紐づくコードを変えずに、URLだけを変えることができるため、直接URLを指定する方法よりも便利な方法です。redirect()で、生成されたURLにリダイレクトされます。
  8. インプットが有効ではない場合、ユーザにはエラーメッセージが表示されます。flash()は、テンプレートを読み込むときに取り出されるメッセージを保存する関数です。
  9. ユーザが最初にURL/auth/registerにアクセスする場合かまたはエラーが発生した場合、登録フォームがあるページが表示される必要があります。render_template()は、次のステップで扱うHTMLを含むテンプレートを読み込みます。

4-3. view関数:Login

この関数は、registerview関数と同じようなパターンになります。

# 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')

registerview関数との違い

  1. ユーザはまずデータベースに照会され、後での使用のために変数に格納されます。
  2. check_password_hash()は、保存された時と同じように提示されたパスワードをハッシュ化し、安全に保存されたパスワードと照合されます。ハッシュ値が一致した場合、パスワードが認証されたことになります。
  3. sessionは、リクエストをまたいでデータを保存するdict型のデータです。ユーザ情報が有効であると確認できた場合、新しいsessionにユーザのidが保存されます。このデータはブラウザに送られるクッキーの中に保存され、続くリクエストで送り返されます。Flaskは、それが不正に書き換えられないよう、そのデータに安全に署名を行います。

今、ユーザのidsessionの中に保存されたので、連続するリクエストの中で利用できるようになります。ユーザがログインしている場合、それぞれのリクエストの最初でこれらの情報は読み取られ、他の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.userNoneとなります。

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関数のエンドポイントはauthblueprintに追加されているので、auth.loginとなります。

チュートリアル4