下町エンジニアの雑多ブログ

東京の端っこでITエンジニアやってます。テックブログを中心に書いていきたいと思います

Flask入門~公式ドキュメントを読み解く~ Day 4 ~

f:id:usktkt:20181019225022p:plain

前回の内容

前回の記事では、データベース周りの設定と、初期化を行いました。

今回はBlueprintとビューを実装していきます。

MVTモデルについて

Webアプリケーションについて勉強したことのある方なら、MVCモデルというワードを聞いたことがあると思います。これはModel、View、Controllerの頭文字で、Ruby on Railsはこの形式を取っています。ざっくり説明するとそれぞれの役割は以下のようになります。

  • Model:ビジネスロジックを記述。データベースアクセスを伴うことが多い
  • View:画面描画を担当。ユーザからの入力も受け付ける
  • Controller:URLを振り分ける司令塔の役割。ビューからのデータをモデルに渡したり、その逆も行う

一方で、FlaskはMVCではなく、MVTという形式を取っています。これは、Model、View、Templateの頭文字となります。 MVCとMVTは、内容はほぼ同じですが、それぞれの役割が異なります。

  • Model : MVCモデルのModelと同じ役割
  • View : MVCモデルのControllerと同じ役割
  • Template : MVCモデルのViewと同じ役割

同じ名前で役割が違うのが、少しややこしいところです。ModelとViewについては今回の記事で、Templateについては次回の記事で見ていくことにします。

Blueprintとは

まずBlueprintとは何かというところからですが、日本語にすると、「青写真」です。「青写真を描く」とか言いますね。設計図や未来図といった意味で使われます。

FlaskでのBlueprintは、機能ごとにファイルを分割してグルーピングしたい場合に使われます。大きなプロジェクトでは機能数が多く、それに伴いコードも膨大になってしまいます。そんな時にそれらを整理して「青写真を描く」といった意味でBlueprintになったのではないかと推測しています。

Blueprintを用いた実装は以下の2段階で行われます。

  1. Blueprintの実装
  2. 実装したBlueprintをFlaskアプリケーションに登録

チュートリアルのflaskrアプリケーションでは、認証機能とブログ投稿機能の2つのBlueprintを作成します。

認証用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')    # Blueprintの実装

これにより、'auth'という名前のBlueprintが生成されます。Blueprintは定義された場所を知る必要があるため、第二引数に__name__を指定しています。url_prefixの値は、Blueprintと関連のあるURLの先頭につけられます。

Blueprintを生成したら、次にアプリケーションに登録します。__init__.pyを開き、最終行のreturn app の前に以下を追記します。

    from . import auth
    app.register_blueprint(auth.bp)    # Blueprintをアプリケーションに登録

    return app  # 最終行

app.register_blueprint()の引数にBlueprint名.bpを指定するとアプリケーションに登録することができます。

認証Blueprintは以下の3つのビューを持ちます。

  • Register (ユーザ登録)
  • Login
  • Logout

それぞれを実装していきましょう。

auth.pyの最終行に以下を追記します。

@bp.route('/register', methods=('GET', 'POST'))
def register():
    if request.method == 'POST':
        username = request.form['username']    # ユーザーフォームからusernameを取り出す
        password = request.form['password']     # ユーザーフォームからpasswordを取り出す
        db = get_db()
        error = None

        if not username:    # ユーザ名が空だった場合
            error = 'Username is required.'
        elif not password:    # パスワードが空だった場合
            error = 'Password is required.'
        elif db.execute(
            'SELECT id FROM user WHERE username = ?', (username,)
        ).fetchone() is not None:    # ユーザ名を条件にデータベースからidを取り出し、すでにidが存在する場合
            error = 'User {} is already registered.'.format(username)

        if error is None:    # ユーザ名とパスワードが入力され、既存ユーザ出ない場合
            db.execute(    # ユーザをデータベースに登録
                'INSERT INTO user (username, password) VALUES (?, ?)',
                (username, generate_password_hash(password))    # パスワードはハッシュ化
            )
            db.commit()
            return redirect(url_for('auth.login')) # ログインページにリダイレクト

        flash(error)

    return render_template('auth/register.html')

@bp.routeはURL /registerとビュー関数(register)を関連づけます。Blueprintのurl_prefixに'/auth'を設定したので、/registerの前に/authがついて/auth/registerにリクエストを受け取った際に、戻り値を返すようになります。登録画面ではGETリクエストもPOSTリクエストもあり得るので、第二引数のmethodsにはタプルで'GET', 'POST'を設定しています。

リクエストメソッドが’POST’の場合、ユーザが入力したフォームからusernameとpasswordを取り出します。そしてusernameとpasswordが空でないことを検証し、usernameを条件にDBからidをselectするクエリを投げます。その結果が空でなければ、そのユーザー名は既に使われているということでエラーを返します。

db.execut内の?はプレースホルダと呼ばれ、usernameと置き換えられます。これにより、SQLインジェクションを防ぐことができます。

fetchone()はクエリの結果1行を返します。結果が無い場合はNoneを返すので、if文でerror変数に値が代入されます。

検証に成功すると、insert文が発行され、データベースにユーザーが登録されます。セキュリティの観点から、データベースに平文のままパスワードを格納するのはよろしく無いので、ハッシュ関数を通した状態で保存しておきます。

ユーザーの登録後、ログインページにリダイレクトしています。url_for()メソッドは第一引数のメソッドへのURLを生成します。つまりここでは、authモジュールのloginメソッドへのURLを生成しています。

ユーザー名とパスワードの検証に失敗(どちらかが空など)した場合、エラー内容が表示されます。flash()を使うことで、動的にエラーメッセージを表示することができます。

ユーザーからのリクエストがGETメソッドだった場合、render_template()メソッドでhtmlを返します。htmlの作成は次回の記事で行う予定です。

続いて、ログインビューを作成します。 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()    # DBに接続
        error = None
        user = db.execute(
            'SELECT * FROM user WHERE username = ?', (username,)
        ).fetchone()

        if user is None:
            error = 'Incorrect username.'
        elif not check_password_hash(user['password'], password):    # パスワードが誤っていた場合(入力したパスワードをハッシュ化して比較)
            error = 'Incorrect password.'

        if error is None:    # 何もエラーがなかった場合
            session.clear()
            session['user_id'] = user['id']
            return redirect(url_for('index'))

        flash(error)

    return render_template('auth/login.html')

ユーザからのリクエストがPOSTだった場合、フォームから取得したユーザー名を条件にDBにselectクエリを投げます。

そして取得したuser情報の検証を行い、問題がなければ新しいセッションににidを格納します。ここで、check_password_hashメソッドは第二引数で指定したパスワードをハッシュ化し、第一引数のDBから取得したハッシュ化されたパスワードを比較し、一致すればTrue、不一致であればFalseを返します。

セッションにidを格納することができたので、次はこのセッションを使ってリクエスト毎にユーザがログイン状態か否かを判定するメソッドを作成します。 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によって登録されたメソッド(ここではload_logged_in_user)は、ビューメソッド(@bp.routeで登録されたメソッド)が実行される前に実行されるようになります。load_logged_in_userはuser_idがセッションに格納されているかのチェックを行い、user_idが存在する場合DBからユーザ情報を取得し、存在しない場合はNoneを設定します。

ログイン機能ができたので、ログアウト機能も実装します。auth.pyに以下を追記してください。

@bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

ログアウトメソッドは非常に簡単で、セッションをクリアして、indexメソッドへリダイレクトさせるだけの実装となっています。

次が最後のメソッドになりますが、今回はブログアプリなので、記事の投稿や削除を行います。記事の投稿や削除を行うにはユーザがログイン状態である必要があります。それらのビューメソッドの前にユーザのログイン状態について確認するメソッドを実装します。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

このメソッドは上記の通り、ブログ記事の投稿や削除のビューメソッドの前に実行され、ユーザがログイン状態でなければログイン画面にリダイレクトさせるものとなります。詳しい使い方についてはブログビューを作成する際に解説しようと思います。

まとめ

今回は少し長くなりましたが、Blueprintの説明や認証周りの実装を行いました。認証の方法はどのアプリケーションでも大きく変わらないので、今回の内容を正しく理解できれば、オリジナルのアプリケーションを作るときにでも使いまわせると思います。

次回は認証時に実際に表示される画面を作っていきたいと思います。