[ Python ] flask-loginでログイン認証を実装してみた
過去の記事でFlaskを使ってCRUDの実装、フォームのバリデーションをしてきました。今回はテストユーザーのセッションを使って簡単なログイン認証機能について調査し、ログイン前画面->ログイン後画面->ログアウトまで作ってきます。
前提
Pythonのライブラリは
- flask
- flask_wtf
- flask-login <- 今回新たに入れます
- sqlalchemy
をpipで入れておくことが前提条件になります。
※上記ライブラリの使用例として「FlaskとSqlAlchemyで簡単なCRUDアプリを作ってみた」をご紹介しています。
また、SQLAlchemyでユーザーデータのDB操作もできるよう、MySQLの起動もしておきましょう。
ディレクトリ構成
以下の構成で作成していきます。
$ 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はクリーンアップされます。
最後に
今回は簡単なログイン認証機能を実装してみました。
限られたユーザーのみがアクセスできるようにして、サービスの幅を広げていきたいですね。
ではこのへんで!