0utputab1e

[ Python ] flask-loginでログイン認証を実装してみた

 2020-07-18
 

過去の記事でFlaskを使ってCRUDの実装フォームのバリデーションをしてきました。今回はテストユーザーのセッションを使って簡単なログイン認証機能について調査し、ログイン前画面->ログイン後画面->ログアウトまで作ってきます。

前提

Pythonのライブラリは

  • flask
  • flask_wtf
  • flask-login <- 今回新たに入れます
  • sqlalchemy

をpipで入れておくことが前提条件になります。

※上記ライブラリの使用例として「FlaskとSqlAlchemyで簡単なCRUDアプリを作ってみた」をご紹介しています。

 
また、SQLAlchemyでユーザーデータのDB操作もできるよう、MySQLの起動もしておきましょう。

※MySQLの簡単な準備は「MysqlをDocker(docker-compose)で立ててみた」を参考にしてみてください。

ディレクトリ構成

以下の構成で作成していきます。

$ tree
.
├── server.py
├── database.py
└── templates
    ├── login.html
    └── mypage.html

1 directory, 7 files

flask-loginのインストール

Flaskではログイン機能の実装に使えるライブラリ「flask-login」が用意されています。

以下のようにpipでインストールします。

$ pip install flask-login

ログイン用モデルの実装

過去の記事「FlaskとSqlAlchemyで簡単なCRUDアプリを作ってみた」の内容を引き継いで実装してみます。

flask-loginでログインセッションの処理のために、特定のプロパティ/メソッド群が実装されたユーザーモデルが必要になります。

アプリケーション作成の途中からログイン機能を入れようとするとき、今まで利用していたユーザーモデルを継承するかたちでログインユーザーモデルを作成することができます。

以下のように実装します。

※もちろん、今までの継承元モデル(Userモデル)でDB操作を続けていくこともできます

#!/home/<user_name>/.virtualenvs/web1/bin/python

import sys
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.sql.functions import current_timestamp
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, TIMESTAMP
from flask_login import UserMixin # <= 新たに追加

DATABASE = 'mysql://%s:%s@%s/%s?charset=utf8mb4' % (
    "docker",  #ユーザー「docker」の
    "docker",  #パスワード「docker」で
    "127.0.0.1:3306",  #MySQLサーバ(http:\/\/127.0.0.1:3306)に接続し
    "test_db", #データベース「test_db」にアクセスします
)

#DB接続用のインスタンスを作成
ENGINE = create_engine(
    DATABASE,
    convert_unicode=True,
    echo=True  #SQLをログに吐き出すフラグ
)

#上記のインスタンスを使って、MySQLとのセッションを張ります
session = scoped_session(
    sessionmaker(
        autoflush = False,
        autocommit = False,
        bind = ENGINE,
    )
)

#以下に書いていくDBモデルのベース部分を作ります
Base = declarative_base()
Base.query = session.query_property()

#DBとデータをやり取りするためのモデルを定義
class User(Base):
    __tablename__ = 'users'
    id = Column('id', Integer, primary_key = True)
    name = Column('name', String(200))
    age = Column('age', Integer)
    description = Column('description', TEXT)
    topics = Column('topics', TEXT)
    created_at = Column('created_at', TIMESTAMP, server_default=current_timestamp())
    updated_at = Column('updated_at', TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))

# 今まで利用していたUserモデルと、ログイン機能用のUserMixinを合成
class LoginUser(UserMixin, User):
    # このモデルを介して認証ユーザーIDを内部で取得するためのメソッド(このメソッド名で作ってあげます)
    def get_id(self):
        return self.id

def main(args):
    Base.metadata.drop_all(bind=ENGINE)
    Base.metadata.create_all(bind=ENGINE)

#このファイルを直接実行したとき、mainメソッドでテーブルを作成する
if __name__ == "__main__":
    main(sys.argv)

元ファイルからの変更箇所は

  • from flask_login import UserMixin
  • 元のUserモデルとUserMixinクラスを統合した認証用Userモデル

です。

これは、flask_loginがセッションで利用するユーザー情報を規定の方法で内部から参照するために必要なため、ここで準備しておきます。

認証ユーザーモデルに必要なID取得メソッド

def get_id(self):
    return self.id  

は、必要に応じて上記のように書き換えることができます。

 
一方で、特にget_idメソッドに何も機能追加しない場合

class LoginUser(UserMixin, User):
  pass

のように単純なクラス統合をすれば、UserMixinクラス備え付けのget_idメソッドを利用することになります。

 
ちなみに、UserMixinクラスは以下の内容になっています。

class UserMixin(object):
    if not PY2:
        __hash__ = object.__hash__

    @property
    def is_active(self):
        return True

    @property
    def is_authenticated(self):
        return True

    @property
    def is_anonymous(self):
        return False

    def get_id(self):
        try:
            return text_type(self.id)
        except AttributeError:
            raise NotImplementedError('No `id` attribute - override `get_id`')

    def __eq__(self, other):
        '''
        Checks the equality of two `UserMixin` objects using `get_id`.
        '''
        if isinstance(other, UserMixin):
            return self.get_id() == other.get_id()
        return NotImplemented

    def __ne__(self, other):
        '''
        Checks the inequality of two `UserMixin` objects using `get_id`.
        '''
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

下2つのメソッドでユーザーの判定時、「get_id」を使うと言われていますね。

メインプログラムの実装

今回は試しに名前(nameカラム)でユーザー検証する場合を例にして、ログインを実装していきます。

※ハッシュ化したパスワードの検証をロジックに入れればパスワード検証も可能ですが、今回はログインの仕組みにフォーカスするので省略します。

  • server.py
from flask import Flask, abort, render_template, request, redirect, url_for
from flask_wtf.csrf import CSRFProtect
import os
from database import *
from flask_login import LoginManager, login_required, login_user, logout_user

login_manager = LoginManager()

app = Flask(__name__)
login_manager.init_app(app)

app.config['SECRET_KEY'] = os.urandom(24) #セッション情報を暗号化するための設定
csrf = CSRFProtect(app)

@login_manager.user_loader
def load_user(user_id):
    return LoginUser.query.filter(LoginUser.id == user_id).one_or_none()

@app.route('/login', methods=['GET'])
def form():
    return render_template('login.html')

@app.route('/login', methods=['POST'])
def login():
    name = request.form.get("name", "")
    try:
        user = LoginUser.query.filter(LoginUser.name == name).one_or_none()
        if user == None:
            return render_template('login.html', error="指定のユーザーは存在しません")
        else:
            login_user(user, remember=True)
    except Exception as e:
        return redirect(url_for('mypage'))
    return redirect(url_for('mypage'))

@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))

@app.route('/mypage', methods=['GET'])
@login_required
def mypage():
    return render_template('mypage.html')

if __name__=="__main__":
    app.debug = True
    app.run(host='0.0.0.0', port="8888")

まず、flask_login機能を現在のアプリケーションで利用するために、ログイン管理用インスタンスを作成します。

そして、既存のFlask(__name__)で作成されているアプリケーションを内包し、初期化します。

login_manager.init_app(app)

次に、

@login_manager.user_loader
def load_user(user_id):
    return LoginUser.query.filter(LoginUser.id == user_id).one_or_none()

では、現在セッションに保存されているユーザーIDから、ユーザー情報を再読み込みするために実装が必要です。

 
引数からはユーザーIDが渡されるので、それを使ってアクセス時点の最新ユーザー情報を読み込んでくれます。

※読み込んだIDが無効な場合、戻り値がNoneになるよう実装しないといけないので注意しましょう。

 
ここでは不正(or 無効)なIDによってユーザー情報の検索が失敗したときNoneを返す必要があるため、失敗時例外が出る「one」ではなく「one_or_none」メソッドを使って取得しています。

あとは、フォームから渡された情報をもとにコントローラメソッド内でユーザーを取得し、

login_user(セッションに保存するユーザーオブジェクト)

でログインさせます。

 
もしログイン時にしかアクセスさせない「アクセス制限」が必要であれば、以下のように

@login_required

というデコレータをメインプログラムのメソッドに付ければOKです。

 
これで、ログイン処理は実装完了です!

テンプレートの実装

ログイン要求時の画面

フォームからユーザー情報を打ち込み、ログインできる画面を作成します。

  • templates/login.html
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <p>{% if error %}{{ error }}{% endif %}</p>
    <form action="/login" method="post">
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
        <div>
            name <input type="text" name="name">
        </div>
        <div>
            <input type="submit">
        </div>
    </form>
</body>
</html>

POST用loginメソッドにフォームを渡せるようにしています。
ここでは名前のみ入力しています。

ログイン失敗時のみ変数errorができるので、もしerrorが存在したらエラー出力するようにしています。

ログイン成功時の画面

ログイン成功時ジャンプする画面も準備しておきましょう。

  • templates/mypage.html
<html>
<head></head>
<body>
    <div>ログインに成功しました。ここはあなたのページです。</div>
    <a href="/logout">ログアウト</a>
</body>
</html>

以上になります。

ログアウト処理について

ログアウトの場合、「/logout」へ通じるリンクを置いておきます。

上記のメインプログラムで記述したlogoutメソッド中にあった

logout_user()

で現在のセッションからログアウトされ、セッションのCookieはクリーンアップされます。

最後に

今回は簡単なログイン認証機能を実装してみました。

限られたユーザーのみがアクセスできるようにして、サービスの幅を広げていきたいですね。

 
ではこのへんで!

 

あわせて読みたい記事

>> Homeに戻る