Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その3〜〜ログイン認証追加〜〜

はじめに

今回は、前回作成したフロントのUIがVue.js、バックエンドのAPIFlaskのWebアプリケーションに対してFlask-Loginを使用してログイン機能を作成します。
Flask-Login

↓前回
kittagon.hateblo.jp

↓前々回 kittagon.hateblo.jp

まずはFlask-Loginとログインフォームの作成に使用するFlask-WTFを以下コマンドでインストールします。

$ pip install flask-login flask_wtf

ユーザーモデルの追加

まずはログインユーザーとなるユーザーモデルをmodels.pyに記述します。
公式サイトにも以下のように記載されており、UserクラスはUserMixinを継承する形で記述すると簡単なようです。

To make implementing a user class easier, you can inherit from UserMixin, which provides default implementations for all of these properties and methods. (It’s not required, though.)

また、ログインフォームはとあえず簡単にflask_wtfを使いLoginFormクラスを作成しておきます。

# backend/models.py


from flask_login import UserMixin
from flask_wtf import FlaskForm
from wtforms import TextField, PasswordField, validators
from sqlalchemy.orm import synonym
from werkzeug import check_password_hash, generate_password_hash

from backend import db

class LoginForm(FlaskForm):
    email = TextField('email', validators=[validators.Required()])
    password = PasswordField('Password', validators=[validators.Required()])
    def __str__(self):
        return '''
<form action="/-/login" method="POST">
<p>%s: %s</p>
<p>%s: %s</p>
%s
<p><input type="submit" name="submit" /></p>
</form>
''' % (self.email.label, self.email,
       self.password.label, self.password,
       self.csrf_token)

class User(UserMixin, db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), unique=True, nullable=False)
    email = db.Column(db.String(255), unique=True, nullable=False)
    _password = db.Column('password', db.String(255), nullable=False)
    active = db.Column(db.Boolean(), default=True)

    def _get_password(self):
        return self._password
    def _set_password(self, password):
        if password:
            password = password.strip()
        self._password = generate_password_hash(password)
    password_descriptor = property(_get_password, _set_password)
    password = synonym('_password', descriptor=password_descriptor)

    # フォームで送信されたパスワードの検証
    def check_password(self, password):
        password = password.strip()
        if not password:
            return False
        return check_password_hash(self.password, password)

    # 認証処理
    @classmethod
    def auth(cls, query, email, password):
        user = query(cls).filter(cls.email==email).first()
        if user is None:
            return None, False
        return user, user.check_password(password)

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Text)
    text = db.Column(db.Text)

    def to_dict(self):
        return dict(
            id=self.id,
            title=self.title,
            text=self.text
        )

    def __repr__(self):
        return '<Entry id={id} title={title!r}>'.format(
            id=self.id, title=self.title)

def init():
    db.create_all()

ログイン処理の追加

次にFlask側の処理の中に実際のログイン処理を記述していきます。
__init__.pyFlask-Loginの初期化
view.pyでログイン画面のルーティングとログイン処理を実装します。

このあたりを参考。
Flask-Loginの使い方 - Qiita
Python Flaskでつくる LDAPログインページ - AAA Blog
Flaskで認証 - Kensuke Kousaka's Blog

config.py

# backend/config.py

import os
class BaseConfig(object):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///backend.db'
    # cookieを暗号化する秘密鍵
    SECRET_KEY = os.urandom(24)
    # csrf token用秘密鍵
    WTF_CSRF_SECRET_KEY = os.urandom(24)

init.py

Flask-loginを使うとcurrent_userlogin_userlogout_userという変数が払い出されて払い出されていい感じにログイン状態の確認ができます。
また、後述するview.pyに記載するルーティングを-というプレフィックスをつけて取り込んでいます。

# backend/__init__.py

from flask import Flask, request, redirect, url_for
from flask_cors import CORS
import requests
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, current_user, login_user, logout_user, login_required # ここを追記

app = Flask('FLASK-VUE',
            static_folder = "./dist/static",
            template_folder = "./dist")
app.config.from_object('backend.config.BaseConfig')

db = SQLAlchemy(app)

######ここから追記######

login_manager = LoginManager()
login_manager.init_app(app)

from backend.models import User

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)

# 未認証の際のリダイレクト先を設定
@login_manager.unauthorized_handler
def unauthorized_callback():
    return redirect(url_for('app.login'))

############# route ##############

from backend.view import view
app.register_blueprint(view, url_prefix="/-")

######ここまで追記######

from backend.api import api
app.register_blueprint(api, url_prefix="/api")
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
@login_required # ここを追記
def catch_all(path):
    if app.debug:
        return requests.get('http://localhost:8080/{}'.format(path)).text
    return render_template("index.html")

view.py

from flask import Blueprint, redirect, request
from flask_login import login_user, logout_user

from backend import db
from backend.models import User, LoginForm

view = Blueprint('app', __name__,
                    template_folder='templates',
                    static_folder='templates/static')

@view.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if form.validate_on_submit():
        user, authenticated = User.auth(db.session.query, form.email.data, form.password.data)
        if authenticated:
            login_user(user, remember=True)
            print('Login successfully.')
            return redirect('/tasks')
        else:
            print('Login failed.')
    return str(form)

@view.route('/logout', methods=['POST'])
def logout():
    logout_user()
    return redirect('/login')

ログアウトボタンの追加

ログインした後のタスク画面にログアウトボタンを追加します。
以下のボタンをTasks.vueの中に記述します。
場所はどこでも結構です。

// frontend/src/component/Tasks.vue

<b-form v-if="show" action="/-/logout" method="post" >
  <b-button type="submit">ログアウト</b-button>
</b-form>

ログインユーザーの作成

まだログインユーザーを作成する画面を作っていないので、
Pythonインタラクティブモードからユーザーを作成し、動作確認をします。

$ python
>>> from backend.models import User, db
>>> User.query.all()
[]
>>> user = User(name='test_user', email='test@example.com')
>>> user._set_password('password')
>>> user
<User (transient 4334868296)>
>>> db.session.add(entry)
>>> db.session.commit()
>>> User.query.all()
[<User 1>]

http://localhost:5000/-/loginを開いてログイン画面が表示され、
'test@example.com'と'password'でログインできればOKです。
また、ログインしていない状態でhttp://localhost:5000/tasksを表示しようとすると
ログイン画面にリダイレクトされます。
ログアウトボタンでログアウトできます。

ログイン画面修正

最初に作成したのは最低限の要素のみ記述したので、もう少しきれいなレイアウトに変更します。
現状は↓のようにフォームを文字列で定義しておき、それを表示しているだけだったので、
画面をテンプレートから呼び出せるよう変更します。

    def __str__(self):
        return '''
<form action="/-/login" method="POST">
<p>%s: %s</p>
<p>%s: %s</p>
%s
<p><input type="submit" name="submit" /></p>
</form>
''' % (self.email.label, self.email,
       self.password.label, self.password,
       self.csrf_token)

テンプレートの作成には、Vue.js側で作成している画面と揃えるために、Flask-Bootstrapを使用します。

Flask-Bootstrap

まずは以下コマンドでインストールします。

$ pip install flask-bootstrap

__init__.pyでflask-bootstrapの初期化をします。

# backend/__init__.py

from flask import Flask, request, redirect, url_for
from flask_cors import CORS
import requests
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, current_user, login_user, logout_user
from flask_bootstrap import Bootstrap # ここを追記

app = Flask('FLASK-VUE',
            static_folder = "./dist/static",
            template_folder = "./dist")
app.config.from_object('backend.config.BaseConfig')

db = SQLAlchemy(app)

bootstrap = Bootstrap(app) # ここを追記

#############以下略#############

view.pyのログイン画面表示を修正します。
テンプレートの呼び出しに加えて、actionをFlaskテンプレートの変数としてurl_forを渡すように修正しています。

# backend/view.py

from flask import Blueprint, redirect, request, render_template, url_for # render_template, url_forを追加

#############略#############

@view.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if form.validate_on_submit():
        user, authenticated = User.auth(db.session.query, form.email.data, form.password.data)
        if authenticated:
            login_user(user, remember=True)
            print('Login successfully.')
            return redirect('/tasks')
        else:
            print('Login failed.')
    # return str(form)
    ## ここを修正
    return render_template('login.html', form=form, action=url_for('app.login'))
#############略#############

models.pyからstr要素を削除します。
また、Loginform内に新たにsubmit要素を定義します。

# backend/models.py

from flask_login import UserMixin
from flask_wtf import FlaskForm
from wtforms import TextField, PasswordField, validators, SubmitField # SubmitFieldの追加
from sqlalchemy.orm import synonym
from werkzeug import check_password_hash, generate_password_hash

from backend import db

class LoginForm(FlaskForm):
    email = TextField('email', validators=[validators.Required()])
    password = PasswordField('Password', validators=[validators.Required()])
    submit = SubmitField('ログイン') # submit追加
    # def __str__(self):の削除

次にログインテンプレートを作成します。
テンプレートフォルダはview.pyにて以下のように定義されています。

view = Blueprint('app', __name__,
                    template_folder='templates',
                    static_folder='templates/static')

view.pyと同階層にtemplatesフォルダを作成します。

app_dir
 ┣manage.py
 ┣frontend/
 ┗backend/
   ┣__init__.py
   ┣api.py
   ┣view.py
   ┣models.py
   ┣config.py
   ┗templates/

templatesフォルダ内にログイン画面用テンプレートを作成します。

<!-- backend/templates/login.html-->
{% import "bootstrap/wtf.html" as wtf %}
{% extends "bootstrap/base.html" %}
{% block title %}This is a login page{% endblock %}
{% block content %}
  <div class="container">
    {{ wtf.quick_form(form, action=action, method="post") }}
  </div>
{% endblock %}

またまた画面デザインは適当ですが、Bootstrapにより良い感じのフォームができました。

まとめ

これで認証までも含めたWebアプリができました。
基本的な機能はほぼ実装することができたので、今後はこれをベースにいろんな機能を実装していきたいと思います。

参考サイト