flask-loginでWebアプリにログイン機能を追加する

pythonでWebアプリを作成するのに、どうしてもログイン機能が必要になりました。Flaskでログイン機能を実装するには、flask-loginが便利なようです。しかし、オブジェクト指向もクラスも理解していない状態で実装するとなると、結構難しかったです。Webアプリの作成は、かなり幅広い知識が必要みたいで、完成までかなり時間がかかりそうです。

Flaskって便利なのですが、使い方を日本語で説明してるサイトが少なくて、エラーが出ると調べるのが面倒なんですよね。

準備

インストール

pip install flask
pip install flask-login
pip install sqlalchemy

import

FlaskとSQLAlchemyも使うので、必要なものをimportしておきます。

SQLAlchemy

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import Integer,String
from sqlalchemy.orm import scoped_session,sessionmaker

Flask

from flask import Flask,render_template
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev'

データベース

sample.sqliteには、こんな感じで1つだけデータを入れています。

idusernamepassword
1dattesardattesar_pass

flask-login

from flask_login import (
    UserMixin,LoginManager,login_manager,
    login_user,login_required,logout_user
    )

engine = create_engine('sqlite:///sample.sqlite')
Base = declarative_base()

class User(UserMixin,Base):
    __tablename__ = 'test'
    id = Column(Integer,primary_key=True)
    username = Column(String,unique=True)
    password = Column(String,nullable=False)

Base.metadata.create_all(engine)
session_user = scoped_session(sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine
))

login_manager = LoginManager()
login_manager.init_app(app)

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

@app.route('/')
def index():
    user = session_user.query(User).filter(User.username == 'dattesar').one_or_none()
    login_user(user)
    return render_template('login_test.html')

@app.route('/logout')
@login_required
def logout():
    logout_user()
    return render_template('logout.html')

こんな感じで、flask-loginの使い方のサイトにサンプルコードがのっていることが多いです。このように書けばログイン機能を実装できるのですが、何をやっているのか全くわかりません。

LoginManager

login_manager = LoginManager()
login_manager.init_app(app)

このあたりは、Login_Managerを作成して初期化しているのだろうな、と想像できますが、中で何をしているかはわかりません。FlaskやSQLAlchemyをimportした時も、中で何をしているかはわからないので、個人的にはそれほど気にはなりません。LoginManagerっていう機能を使っているんだろうな、という感じです。

@login_manager.user_loader

個人的に、すごく気になるのはここです。

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

この関数、一体何をしてるの?

サンプルコードがのっているサイトでも、さらっと流されていてあまり説明されていないので、ちょっと調べてみました。

user_loader

This sets the callback for reloading a user from the session. The function you set should take a user ID (a unicode) and return a user object, or None if the user does not exist.

Parameters: callback (callable) – The callback for retrieving a user object.

https://flask-login.readthedocs.io/en/latest/#flask_login.LoginManager.user_loader

get_id()

This method must return a unicode that uniquely identifies this user, and can be used to load the user from the user_loader callback. Note that this must be a unicode – if the ID is natively an int or some other type, you will need to convert it to unicode.

https://flask-login.readthedocs.io/en/latest/#your-user-class

なるほど、よくわかりません。

UserMixin

UserMixinのソースコードを見てみます。

class UserMixin(object):
”’ This provides default implementations for the methods that Flask-Login expects user objects to have. ”’
(中略)
def get_id(self):
 try:
  return text_type(self.id)
(以下略)

https://flask-login.readthedocs.io/en/latest/_modules/flask_login/mixins.html#UserMixin

なるほど、何となくわかってきました。

UserMixinのidカラムをテキストタイプで返す感じなのでしょうか?だから、idカラムがintなら、intに変換してねって書いてあったんですね。

class User(UserMixin,Base):
    __tablename__ = 'test'
    id = Column(Integer,primary_key=True)
    username = Column(String,unique=True)
    password = Column(String,nullable=False)

検証(UserMixin)

class User(UserMixin,Base):
    __tablename__ = 'test'
    username = Column(String,primary_key=True)
    password = Column(String,nullable=False)

試しに、idカラム無しで作成してみると・・・

NotImplementedError: No `id` attribute - override `get_id`

やっぱりエラーが出ます。

ということは、このget_id()を書き換えれば、Userクラスの他のカラム値を返せるみたいです。

usernameを返す場合は、こんな感じです。

class User(UserMixin,Base):
    __tablename__ = 'test'
    username = Column(String,primary_key=True)
    password = Column(String,nullable=False)

    def get_id(self):
        return self.username

UserMixinとuser_loaderのイメージ

  1. UserMixinのget_id()の戻り値を関数load_user()の引数に
  2. その値を元に、idカラムがIntegerなのでint型に変換して、Userクラスのデータベースからデータを入手

よくわかっていませんが、イメージ的にはこんな感じなのでしょうか?

SQLAlchemy

query.get()

flask-loginと一緒に使っているので、最初は混同していましたが、session_user.query(User).get(int(user_id))の部分は、SQLAlchemyの機能です。get()を使ったことがなかったので、調べてみます。

method sqlalchemy.orm.Query.get(ident)
  Return an instance based on the given primary key identifier, or None if not found.

https://docs.sqlalchemy.org/en/14/orm/query.html

要するに、primary keyに設定されているカラムから取ってきますよ、

適合するデータがなかったりprimary keyが無いとNoneを返しますよ、ということみたいです。

それで、よくあるサンプルコードでは、idカラムがあって、しかも、idカラムがprimary keyに設定されているから、エラーがでないんですね。

UserMixin分割

UserMixinを分けて書くこともできるようです。

engine = create_engine('sqlite:///sample.sqlite')
Base = declarative_base()

class User(Base):
    __tablename__ = 'test'
    id = Column(Integer,primary_key=True)
    username = Column(String,unique=True)
    password = Column(String,nullable=False)

class LoginUser(UserMixin,User):
    pass

Base.metadata.create_all(engine)
session_user = scoped_session(sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine
))

login_manager = LoginManager()
login_manager.init_app(app)

@login_manager.user_loader
def load_user(user_id):
    return session_user.query(LoginUser).get(int(user_id))

@app.route('/')
def index():
    user = session_user.query(LoginUser).filter(LoginUser.username == 'dattesar').one_or_none()
    login_user(user)
    return render_template('login_test.html')

@app.route('/logout')
@login_required
def logout():
    logout_user()
    return render_template('logout.html')

参照サイト

https://flask-login.readthedocs.io/en/latest/

https://docs.sqlalchemy.org/en/14/orm/query.html