- flask-loginの使い方を理解する
- サインアップ/ログイン認証/ログアウト機能の実装方法を理解する
- バリデーションチェックやデータベース操作を理解する
上記をまとめ、具体的なコードやflask-loginの使い方を重点的に解説します。
認証アプリのディレクトリ構成
本記事では、以下のディレクトリ構成で認証アプリを開発します。
flask-login
├── .env
├── apps
│ ├── app.py
│ ├── config.py
│ ├── static
│ │ └── bootstrap.min.css
│ └── auth
│ ├── __init__.py
│ ├── forms.py
│ ├── models.py
│ ├── static/css
│ │ └── style.css
│ ├── templates
│ │ └── auth
│ │ ├── base.html
│ │ ├── index.html
│ │ ├── login.html
│ │ └── signup.html
│ └── views.py
├── local.sqlite
└── migrations
- flask-login:開発用ディレクトリ名
- apps:格納されたアプリを管理するディレクトリ
- static/css:静的ファイルのディレクトリ
- auth:認証アプリの関連ファイルを格納したディレクトリ
- templates:HTMLファイルを格納したディレクトリ
各ファイルについては、後述の中で解説していきます。
認証アプリの完成画面は、以下になります。
Flaskを利用するためのPython環境構築
Pythonで開発を実施する際、開発プロジェクトごとに専用の実行環境を作成し切り替えることができます。
また、開発用として一時的に作成する実行環境を「仮想環境」と呼びます。
ローカル環境で開発を進めるにあたり、venvモジュールを利用し仮想環境を構築します。
venvは、Pythonに標準搭載された仮想環境用モジュールです。
venvを利用することで、プロジェクトごとに分離したPython実行環境を構築できるため、各実行環境でそれぞれのパッケージを管理できます。
Pythonによる仮想環境の構築方法が知りたい人は、「【Python】ダウンロードとインストール方法から開発環境構築まで解説!」を一読ください。
Flaskのインストール
本記事では、WindowsPCによる任意のディレクトリにてFlaskをインストールしています。
インストール自体は簡単で、以下のコマンドを実行しましょう。
pip install flask
上記のコマンドを実行すると、インストールが完了します。
FlaskによるWebアプリ開発の事前準備
任意で作成したディレクトリにcdコマンドで移動後、仮想環境を作成します。
py -m venv venv
次に、仮想環境をアクティベート(有効化)します。
./venv/Scripts/Activate
pip listを利用するとパッケージの状況が確認できます。
インストールしたのちにpip listでパッケージ状況を確認すると以下のように表示されます。
(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package Version
------------ -------
blinker 1.8.2
click 8.1.7
colorama 0.4.6
Flask 3.0.3
itsdangerous 2.2.0
Jinja2 3.1.4
MarkupSafe 2.1.5
pip 24.0
Werkzeug 3.0.4
さらに、本プログラムでは環境変数の設定ファイルとして.envファイルを利用します。
.envファイルを利用すると、アプリ単位で環境変数を設定できます。
以下のコマンドでpython-dotenvをインストールします。
pip install python-dotenv
pip listで確認します。
(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package Version
------------- -------
blinker 1.8.2
click 8.1.7
colorama 0.4.6
Flask 3.0.3
itsdangerous 2.2.0
Jinja2 3.1.4
MarkupSafe 2.1.5
pip 24.0
python-dotenv 1.0.1
Werkzeug 3.0.4
flaskのインストールが完了したので認証アプリを開発する準備が整いました。
ログイン機能を実装した認証アプリ開発
本記事では、以下の流れで開発を進めます。
- Blueprintによるアプリ登録
- SQLAlchemyとMigrateの事前準備
- コンフィグ設定
- データベースを使ったCRUD機能作成
- サインアップ機能の作成
- ログイン機能の作成
- ログアウト機能の作成
上記の流れで開発していきます。
Blueprintによるアプリ登録
始めに、以下のディレクトリとファイルを作成します。
flask-login
├── .env
└── apps
├── app.py
└── auth
└── views.py
Blueprintによるアプリ登録を実施します。
# Flaskのインポート
from flask import Flask
def create_app():
# Flaskのインスタンス作成
app = Flask(__name__)
# authパッケージからviewsをインポート
from apps.auth import views as auth_views
# register_blueprintを利用してviewsのauthappをアプリへ登録
app.register_blueprint(auth_views.auth, url_prefix="/auth")
return app
- flaskのインポート
- flaskのインスタンス作成
- authパッケージからviewsをインポート
- register_blueprintを利用してviewsのauthappをアプリへ登録
from flask import Flask
flaskをインポートし、Flaskフレームワークの機能を利用できるようにします。
def create_app():
app = Flask(__name__)
from apps.auth import views as auth_views
app.register_blueprint(auth_views.auth, url_prefix="/auth")
return app
create_app関数を作成し、appへFlaskインスタンスを作成します。
本プログラムで開発するauthパッケージからviews.pyをインポートします。
Blueprint機能によるregister_blueprint関数を利用し、authアプリを登録します。
また、url_prefixに/authを指定し、views.pyのエンドポイント全てのURLがauthから始まるよう設定します。
次に、views.pyにてBlueprintによるアプリを作成します。
from flask import Blueprint
auth = Blueprint(
"auth",
__name__,
template_folder = "templates",
static_folder = "static",
)
@auth.route("/")
def index():
return 'Hello World!'
- Blueprintのインポート
- Blueprintにてauthを作成
- デコレータ関数にてURLマッピング
- index関数で任意の文字列を出力
from flask import Blueprint
作成するauthアプリのviews.pyでは、Blueprintをインポートします。
auth = Blueprint(
"auth",
__name__,
template_folder = "templates",
static_folder = "static",
)
authアプリをBlueprintオブジェクトで生成します。
@auth.route("/")
def index():
return 'Hello World!'
ひとまず、デコレータ関数にてURLマッピングしており、任意の文字列を出力しておきます。
また、.envファイルにて環境変数を設定します。
FLASK_APP = apps.app:create_app
FLASK_DEBUG = 1
アプリを生成するのが関数の場合、「モジュール:関数」と指定します。
また、アプリを生成する関数名がcreate_app関数の場合、Flaskが自動でcreate_app関数を呼び出せます。
これらのディレクトリ/ファイルを起点にコードを追加していきます。
一度、flask routesコマンドでルーティングの確認します。
(venv) PS C:\Users\sugir\Documents\flask-login> flask routes
Endpoint Methods Rule
----------- ------- ----------------------------
auth.index GET /auth/
auth.static GET /auth/static/<path:filename>
static GET /static/<path:filename>
ルートが割り当てられていることが分かります。
次に、flask runコマンドを実行し127.0.0.5000/auth/にアクセスしてみましょう。
SQLAlchemyとMigrateの事前準備
次に、データベースを利用するための事前準備を行います。
- SQLAlchemyのインストール
- Migrateのインストール
上記どちらもflaskの拡張機能としてインストールできます。
SQLAlchemyとMigrateのインストール
pip installコマンドでSQLAlchemyとMigrateをインストールします。
pip install flask-sqlalchemy
pip install flask-migrate
pip listコマンドでパッケージを確認します。
(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package Version
----------------- -------
alembic 1.13.2
blinker 1.8.2
click 8.1.7
colorama 0.4.6
Flask 3.0.3
Flask-Migrate 4.0.7
Flask-SQLAlchemy 3.1.1
greenlet 3.1.0
itsdangerous 2.2.0
Jinja2 3.1.4
Mako 1.3.5
MarkupSafe 2.1.5
pip 24.0
python-dotenv 1.0.1
SQLAlchemy 2.0.35
typing_extensions 4.12.2
Werkzeug 3.0.4
インストールが完了したのでapp.pyにて利用準備を実施します。
SQLAlchemyとMigrateの利用準備
ここからはSQLAlchemyとMigrateの利用準備のため、app.pyにコードを追加します。
# Flaskのインポート
from flask import Flask
# SQLAlchemyのインポート
from flask_sqlalchemy import SQLAlchemy
# Migrateのインポート
from flask_migrate import Migrate
# SQLAlchemyのインスタンス作成
db = SQLAlchemy()
def create_app():
# Flaskのインスタンス作成
app = Flask(__name__)
# SQLalchemyとアプリを連携
db.init_app(app)
# Migrateとアプリを連携
Migrate(app, db)
# authパッケージからviewsをインポート
from apps.auth import views as auth_views
# register_blueprintを利用してviewsのauthappをアプリへ登録
app.register_blueprint(auth_views.auth, url_prefix="/auth")
return app
- SQLAlchemyのインポート
- Migrateのインポート
- SQLAlchemyのインスタンス作成
- SQLalchemyとアプリを連携
- Migrateとアプリを連携
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
先ほどインストールしたSQLAlchemyとMigrateをインポートします。
db = SQLAlchemy()
変数dbへSQLAlchemyをインスタンス化します。
db.init_app(app)
Migrate(app, db)
create_app関数内にて、SQLAlchemyとMigrateをアプリに連携させます。
models.pyの作成と実装
次に、以下のディレクトリに__init__.pyとmodels.pyを追加で作成します。
flask-login
├── .env
└── apps
├── app.py
└── auth
├── __init__.py
├── models.py
└── views.py
ファイル作成が完了したら、models.pyにコードを追加します。
from apps.app import db
from werkzeug.security import generate_password_hash
# db.Modelを継承したUserクラス作成
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, index=True)
email = db.Column(db.String, unique=True, index=True)
password_hash = db.Column(db.String)
@property
def password(self):
raise AttributeError("読み取り不可")
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
- apps.appからdbをインポート
- generate_password_hashをインポート
- db.Modelを継承したUserクラス作成
- テーブル名の指定
- カラムの定義
- パスワードをセットするプロパティ作成
- passwordのsetter関数でハッシュ化したパスワードをセット
from apps.app import db
from werkzeug.security import generate_password_hash
apps.appからSQLAlchemyをインスタンス化したdbをインポートします。
また、werkzeug.securityから入力されたパスワードをハッシュ化するgenerate_password_hash関数をインポートします。
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, index=True)
email = db.Column(db.String, unique=True, index=True)
password_hash = db.Column(db.String)
@property
def password(self):
raise AttributeError("読み取り不可")
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
db.Modelを継承したUserクラスを作成し、テーブル名を指定します。
作成したいカラム(列)を定義し、パスワードをセットするプロパティを作成します。
また、passwordのsetter関数にてパスワードをハッシュ化してpassword_hashに値をセットしています。
__init__.pyによるモデルの宣言
モデルを作成できたら、__init__.pyにてmodels.pyをインポートします。
import apps.auth.models
コンフィグ設定
次に、以下のディレクトリにconfig.pyを追加で作成します。
flask-login
├── .env
└── apps
├── app.py
├── config.py
└── auth
├── __init__.py
├── models.py
└── views.py
ファイル作成が完了したら、config.pyにコードを追加します。
from pathlib import Path
basedir = Path(__file__).parent.parent
# BaseConfigクラスを作成
class BaseConfig:
SECRET_KEY = "{SECRET_KEY}"
# BaseConfigクラスを継承しLocalConfigクラスを作成
class LocalConfig(BaseConfig):
SQLALCHEMY_DATABASE_URI = f"sqlite:///{basedir/'local.sqlite'}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
# config辞書にマッピング
config = {
"local": LocalConfig,
}
- Pathをインポート
- baseのパスになるbasedirを作成
- BaseConfigクラス作成
- config辞書を作成
config設定は、アプリにおいてどのように管理するか重要です。
開発環境・ステージング環境・本番環境など各環境で設定すべきコンフィグの値が変わるためです。
本記事では、環境を簡易に変更できるfrom_object形式で設定しています。
以下は、コンフィグ設定時に利用される環境設定の方法です。
- from_objectを使う方法
- from_mappingを使う方法
- from_envvarを使う方法
- from_pyfileを使う方法
- from_fileを使う方法
また、データベースを利用する際にセッションを利用するため、SECRET_KEYの設定が必要になります。
そのため、config設定したあとにapp.pyにインポートしcreate_appへ引数を持たせ、コンフィグを読み込ませます。
# Flaskのインポート
from flask import Flask
# SQLAlchemyのインポート
from flask_sqlalchemy import SQLAlchemy
# Migrateのインポート
from flask_migrate import Migrate
# configをインポート
from apps.config import config
# SQLAlchemyのインスタンス作成
db = SQLAlchemy()
def create_app(config_key):
# Flaskのインスタンス作成
app = Flask(__name__)
# config_keyにマッチする環境のコンフィグを読み込む
app.config.from_object(config[config_key])
# SQLalchemyとアプリを連携
db.init_app(app)
# Migrateとアプリを連携
Migrate(app, db)
# authパッケージからviewsをインポート
from apps.auth import views as auth_views
# register_blueprintを利用してviewsのauthappをアプリへ登録
app.register_blueprint(auth_views.auth, url_prefix="/auth")
return app
- configのインポート
- create_appに引数config_keyを持たせる
- config_keyにマッチする環境のコンフィグを読み込む
from apps.config import config
ppsディレクトリに存在するconfig.pyをインポートします。
app.config.from_object(config[config_key])
create_app関数にconfig_keyといった引数を持たせ、config.pyで作成した辞書から指定の値を読み込めるよう設定しています。
次に、環境設定ファイルである.envファイルに変更を追加します。
FLASK_APP = apps.app:create_app("local")
FLASK_DEBUG = 1
config.pyで作成したconfig辞書からlocalを読み込むように設定しています。
データベースの初期化とマイグレーション
次に、データベースの初期化とマイグレーションファイルを作成します。
マイグレーションファイルは、データベースの設計書のようなものです。
マイグレーションファイルを実行することで記述してきた内容がデータベースに反映されます。
データベースの初期化とマイグレーションを実施するflask dbコマンドは以下になります。
- flask db init
- flask db migrate
- flask db upgrade
- flask db downgrade
flask db init
flask db initコマンドは、データベースを初期化します。
ここでは、flask db initコマンドを実行するとappsディレクトリ配下にmigrationsディレクトリが作成されます。
(venv) PS C:\Users\sugir\Documents\flask-login> flask db init
Creating directory 'C:\\Users\\sugir\\Documents\\flask-login\\migrations' ... done
Creating directory 'C:\\Users\\sugir\\Documents\\flask-login\\migrations\\versions' ... done
Generating C:\Users\sugir\Documents\flask-login\migrations\alembic.ini ... done
Generating C:\Users\sugir\Documents\flask-login\migrations\env.py ... done
Generating C:\Users\sugir\Documents\flask-login\migrations\README ... done
Generating C:\Users\sugir\Documents\flask-login\migrations\script.py.mako ... done
Please edit configuration/connection/logging settings in 'C:\\Users\\sugir\\Documents\\flask-login\\migrations\\alembic.ini' before proceeding.
flask db migrate
flask db migrateコマンドは、データベースのマイグレーションファイルを作成します。
flask db migrateコマンドを実行すると、Model定義をもとにmigrations/versions配下にPythonファイルでデータベースに適用する前の情報が生成されます。
(venv) PS C:\Users\sugir\Documents\flask-login> flask db migrate
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added index ''ix_users_email'' on '('email',)'
INFO [alembic.autogenerate.compare] Detected added index ''ix_users_username'' on '('username',)'
Generating C:\Users\sugir\Documents\flask-login\migrations\versions\e488d28ab7fa_.py ... done
flask db upgrade
flask db upgradeコマンドは、マイグレーション情報を実際にデータベースに反映します。
ここでは、flask db upgradeコマンドを実行するとusersテーブルが生成されます。
(venv) PS C:\Users\sugir\Documents\flask-login> flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> e488d28ab7fa, empty message
VSCodeでデータベースを確認する
VSCodeでデータベースの内容を確認できるように設定する方法です。
VSCodeの左サイドバーにある拡張機能アイコンをクリックし、検索バーで「SQLite」を検索します。
作成されたデータベースファイル(ここではlocal.sqlite)を右クリックし、「Open Database」を選択すると「SQLITE EXPLORER」が左下に表示されます。
データベースの作成と操作が実行できるようになりました。
データベースを使ったCRUD機能作成
ここでは、データベースを利用して以下の機能を作成していきます。
実際には、Create(ユーザー作成)とRead(読み込み)になります。
- サインアップ機能の作成
- ログイン機能の作成
- ログアウト機能の作成
また、上記の各機能を作る上で必要になるflask-loginをインストールします。
pip install flask-login
(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package Version
----------------- -------
alembic 1.13.2
blinker 1.8.2
click 8.1.7
colorama 0.4.6
Flask 3.0.3
Flask-Login 0.6.3
Flask-Migrate 4.0.7
Flask-SQLAlchemy 3.1.1
greenlet 3.1.0
itsdangerous 2.2.0
Jinja2 3.1.4
Mako 1.3.5
MarkupSafe 2.1.5
pip 24.0
python-dotenv 1.0.1
SQLAlchemy 2.0.35
typing_extensions 4.12.2
Werkzeug 3.0.4
flask-loginのインストールが確認できました。
サインアップ機能の作成
サインアップ機能作成における主な手順は以下になります。
- flask-loginと連携
- サインアップ機能のフォームクラス作成
- Userモデルの更新
- サインアップ機能のエンドポイント作成
- サインアップ機能のテンプレート作成
- 動作確認
flask-loginと連携
始めに、app.pyにてflask-loginをインポート及び連携を図ります。
# Flaskのインポート
from flask import Flask
# SQLAlchemyのインポート
from flask_sqlalchemy import SQLAlchemy
# Migrateのインポート
from flask_migrate import Migrate
# configをインポート
from apps.config import config
# flask-loginのLoginManagerクラスをインポート
from flask_login import LoginManager
# SQLAlchemyのインスタンス作成
db = SQLAlchemy()
# LoginManagerをインスタンス化する
login_manager = LoginManager()
# login_view属性に未ログイン時にリダイレクトするエンドポイントを指定
login_manager.login_view = "auth.signup"
# login_message属性にログイン後に表示するメッセージを指定
# ここでは何も表示しないよう空を指定
login_manager.login_message = ""
def create_app(config_key):
# Flaskのインスタンス作成
app = Flask(__name__)
# config_keyにマッチする環境のコンフィグを読み込む
app.config.from_object(config[config_key])
# SQLalchemyとアプリを連携
db.init_app(app)
# Migrateとアプリを連携
Migrate(app, db)
# login_managerとアプリ連携
login_manager.init_app(app)
# authパッケージからviewsをインポート
from apps.auth import views as auth_views
# register_blueprintを利用してviewsのauthappをアプリへ登録
app.register_blueprint(auth_views.auth, url_prefix="/auth")
return app
- flask-loginのLoginManagerクラスをインポート
- LoginManagerをインスタンス化する
- login_view属性に未ログイン時にリダイレクトするエンドポイントを指定
- ここでは何も表示しないよう空を指定
- login_managerとアプリ連携
from flask_login import LoginManager
インストールしたflask_loginからLoginManagerをインストールします。
login_manager = LoginManager()
login_manager.login_view = "auth.signup"
login_manager.login_message = ""
任意の変数へLoginManagerをインスタンス化します。
また、login_view属性に未ログイン時にリダイレクトするエンドポイントを指定します。
login_message属性は記述しない場合、デフォルトで英語のメッセージが出力されます。
そのため、何も表示しないように空を設定しています。
login_manager.init_app(app)
create_app関数内にて、login_managerをアプリ連携します。
サインアップ機能のフォームクラス作成
ここでは、サインアップ機能のフォームクラスを作成するため、forms.pyのファイル作成します。
また、フォームの拡張機能であるflask-wtfを利用します。
flask-wtfはバリデーションチェックやCSRF対策の機能を持ったフォーム作成できます。
フォーム作成時のメリットがあるため、インストールしておきましょう。
さらに、email属性のバリデータをチェックする際にはemail-validatorをインストールする必要があります。
こちらもインストールしておきましょう。
pip install flask-wtf
pip install email-validator
(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package Version
----------------- -------
alembic 1.13.2
blinker 1.8.2
click 8.1.7
colorama 0.4.6
dnspython 2.6.1
email_validator 2.2.0
Flask 3.0.3
Flask-Login 0.6.3
Flask-Migrate 4.0.7
Flask-SQLAlchemy 3.1.1
Flask-WTF 1.2.1
greenlet 3.1.0
idna 3.10
itsdangerous 2.2.0
Jinja2 3.1.4
Mako 1.3.5
MarkupSafe 2.1.5
pip 24.0
python-dotenv 1.0.1
SQLAlchemy 2.0.35
typing_extensions 4.12.2
Werkzeug 3.0.4
WTForms 3.1.2
インストールの確認ができたら、ファイルを作成します。
flask-login
├── .env
└── apps
├── app.py
├── config.py
└── auth
├── __init__.py
├── forms.py
├── models.py
└── views.py
# flask_wtfからFlaskFormクラスをインポート
from flask_wtf import FlaskForm
# wtformsから各フィールドをインポート
from wtforms import PasswordField, StringField, SubmitField
# wtformsから各バリデータをインポート
from wtforms.validators import DataRequired, Email, Length
# サインアップフォームクラス作成
class SignUpForm(FlaskForm):
username = StringField(
"ユーザー名",
validators = [
DataRequired("ユーザー名は必須です。"),
Length(1, 30, "30文字以内で入力してください。"),
],
)
email = StringField(
"メールアドレス",
validators = [
DataRequired("メールアドレスは必須です。"),
Email("メールアドレス形式で入力してください。"),
],
)
password = PasswordField(
"パスワード",
validators = [
DataRequired("パスワードは必須です。"),
],
)
submit = SubmitField("Sign up")
- flask_wtfからFlaskFormクラスをインポート
- wtformsから各フィールドをインポート
- wtformsから各バリデータをインポート
- サインアップフォームクラス作成
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Email, Length
flask_wtfからFlaskFormをインポートします。
また、HTMLフォームのパスワード、テキスト、サブミットフィールドをインポートします。
さらに、HTMLフォームからサブミットされた際にバリデートするために必要なバリデータをインポートしています。
class SignUpForm(FlaskForm):
username = StringField(
"ユーザー名",
validators = [
DataRequired("ユーザー名は必須です。"),
Length(1, 30, "30文字以内で入力してください。"),
],
)
email = StringField(
"メールアドレス",
validators = [
DataRequired("メールアドレスは必須です。"),
Email("メールアドレス形式で入力してください。"),
],
)
password = PasswordField(
"パスワード",
validators = [
DataRequired("パスワードは必須です。"),
],
)
submit = SubmitField("Sign up")
FlaskFormを継承したサインアップフォームクラスを作成します。
また、ユーザー名、メールアドレス、パスワードをDataRequiredにて必須項目としています。
ユーザー名は30文字制限、メールアドレスは形式制限してます。
Userモデルの更新
model.pyのコード追加を実施します。
ここでは、Userモデルをログイン機能で利用するために更新します。
flask-loginは、UserMixinクラスが用意されています。
UserモデルにUserMixinを継承することで、以下のプロパティ/メソッドを定義せず利用できるメリットがあります。
プロパティ/メソッド | 説明 |
---|---|
is_authenticated | ログイン時はtrue,、未ログイン時はfalseを返す関数 |
is_active | ユーザーがアクティブ時はtrue、非アクティブ時はfalseを返す関数 |
is_anonymous | ログインユーザーはfalse、匿名ユーザーはtrueを返す関数 |
get_id | ログインユーザーのユニークIDを取得するプロパティ |
# login_managerのインポート
from apps.app import db, login_manager
# flask-loginからUserMixinクラスをインポート
from flask_login import UserMixin
# check_password_hashを追加インポート
from werkzeug.security import generate_password_hash, check_password_hash
# db.Model, UserMixinを継承したUserクラス作成
class User(db.Model, UserMixin):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, index=True)
email = db.Column(db.String, unique=True, index=True)
password_hash = db.Column(db.String)
@property
def password(self):
raise AttributeError("読み取り不可")
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
# パスワードチェックする
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
# メールアドレス重複チェックする
def is_duplicate_email(self):
return User.query.filter_by(email=self.email).first() is not None
# ログインユーザー情報を取得する関数作成
@login_manager.user_loader
def load_user(user_id):
return User.query.get(user_id)
- login_managerのインポート
- flask-loginからUserMixinクラスをインポート
- check_password_hashを追加インポート
- パスワードチェック用関数作成
- メールアドレス重複チェック用関数作成
- ログインユーザー情報を取得する関数作成
from apps.app import db, login_manager
apps.appからlogin_managerを追加インポートします。
from werkzeug.security import generate_password_hash, check_password_hash
werkzeug.securityから入力されたパスワードが正しいかチェックするために、check_password_hashを追加インポートします。
class User(db.Model, UserMixin):
flask-loginで用意されているUserMixinクラスを利用するため、Userクラスに継承させます。
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
def is_duplicate_email(self):
return User.query.filter_by(email=self.email).first() is not None
パスワードをチェックするverify_password関数を作成し、入力されたパスワードがDBのハッシュ化されたパスワードと一致してるかチェックします。
一致する場合はtrueを返し、一致しない場合はfalseを返します。
また、メールアドレス重複をチェックするis_duplicate_email関数を作成します。
DBに同一のメールアドレスを持つレコードがある場合はtrueを返し、レコードがない場合はfalseを返します。
@login_manager.user_loader
def load_user(user_id):
return User.query.get(user_id)
ログインしているユーザー情報を取得するload_user関数を作成し、@login_manager.user_loaderといったデコレータを付与しています。
load_user関数はflask_loginがログインユーザー情報を取得し利用するため、ユーザーのユニークIDを引数としDBから特定ユーザーを取得し返す必要があります。
サインアップ機能のエンドポイント作成
認証機能の作成が完了したので、サインアップ機能のエンドポイントを作成します。
from apps.app import db
from apps.auth.forms import SignUpForm
from apps.auth.models import User
from flask import Blueprint, render_template, flash, url_for, redirect, request
from flask_login import login_user, login_required
auth = Blueprint(
"auth",
__name__,
template_folder = "templates",
static_folder = "static",
)
@auth.route("/")
def index():
# TOPページへのアクセスはサインアップページに遷移させる
return redirect(url_for("auth.signup"))
@auth.route("/auth_ok")
@login_required
def auth_ok():
return render_template("index.html")
# signupエンドポイントを作成する
@auth.route("/signup", methods=["GET", "POST"])
def signup():
# SignUpFormをインスタンス化
signform = SignUpForm()
if signform.validate_on_submit():
user = User(
username = signform.username.data,
email = signform.email.data,
password = signform.password.data,
)
# メールアドレス重複チェック
if user.is_duplicate_email():
flash("指定のメールアドレスは登録済みです")
return redirect(url_for("auth.signup"))
# ユーザー情報を登録する
db.session.add(user)
db.session.commit()
# ユーザー情報をセッションに格納
login_user(user)
# GETパラメータにnextキーが存在し値がない場合にToDo一覧へ
next_ = request.args.get("next")
if next_ is None or not next_.startswith("/"):
next_ = url_for("auth.auth_ok")
return redirect(next_)
return render_template("auth/signup.html", form=signform)
- dbのインポート
- SignUpFormのインポート
- render_template, flash, url_for, redirect, requestのインポート
- login_user, login_requiredのインポート
- signupエンドポイント作成
- SignUpFormのインスタンス作成
- メールアドレス重複チェック
- ユーザー情報をデータベースへ登録
- ユーザー情報をセッションに格納
- nextキーによるリダイレクト
from apps.app import db
from apps.auth.forms import SignUpForm
from apps.auth.models import User
from flask import Blueprint, render_template, flash, url_for, redirect, request
from flask_login import login_user, login_required
app.pyからdbをインポートしています。
form.pyで作成したSignUpFormクラスをインポートします。
models.pyで作成したUserクラスをインポートします。
flaskからそれぞれrender_template、flash、url_for、redirect、requestをインポートします。
flask_loginからlogin_userとlogin_requiredをインポートします。
@auth.route("/signup", methods=["GET", "POST"])
def signup():
signform = SignUpForm()
if signform.validate_on_submit():
user = User(
username = signform.username.data,
email = signform.email.data,
password = signform.password.data,
)
if user.is_duplicate_email():
flash("指定のメールアドレスは登録済みです")
return redirect(url_for("auth.signup"))
db.session.add(user)
db.session.commit()
login_user(user)
next_ = request.args.get("next")
if next_ is None or not next_.startswith("/"):
next_ = url_for("auth.auth_ok")
return redirect(next_)
return render_template("auth/signup.html", form=signform)
デコレータにてsignupエンドポイントを作成し、methodsにGETとPOSTを指定します。
signup関数では、SignUpformクラスをインスタンス化します。
もし、サブミットされた場合はフォーム内容をバリデートチェックします。
フォームデータが通過すれば、ユーザークラスを生成し、変数に入力された各データを格納します。
次に、メールアドレス重複チェックを実施しています。
バリデートチェックとメールアドレス重複チェックを通過すれば、DBにユーザーを登録します。
@auth.route("/auth_ok")
@login_required
def auth_ok():
return render_template("index.html")
ユーザー認証を確認するため、上記のエンドポイントを作成しています。
@login_requiredといったデコレータ関数を追加することで、該当のエンドポイントはログインしないとアクセスできない仕様にできます。
サインアップ機能のテンプレート作成
次に、サインアップ機能を反映させたテンプレートを作成します。
また、apps/staticにはCSSフレームワークであるBootStrapを採用しています。
以下、BootStrapのダウンロードサイトになります。
flask-login
├── .env
└── apps
├── app.py
├── config.py
├── static
│ └── bootstrap.min.css
└── auth
├── __init__.py
├── forms.py
├── models.py
├── static/css
│ └── style.css
├── templates
│ └── auth
│ ├── base.html
│ ├── index.html
│ └── signup.html
└── views.py
テンプレート作成では、共通テンプレートとしてbase.htmlから作成します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AuthApp</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" />
<link rel="stylesheet" href="{{ url_for('auth.static', filename='css/style.css') }}" />
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<ul class="navbar-nav flex-row">
<li class="nav-item mt-1">
<a class="navbar-brand" href="{{ url_for('auth.auth_ok') }}">AuthApp</a>
</li>
</ul>
{% if current_user.is_authenticated %}
<ul class="navbar-nav flex-row">
<li class="nav-item pe-3">
<span class="nav-link">{{ current_user.username }}</span>
</li>
<li class="nav-item pe-2">
<a href="{{ url_for('auth.logout') }}" class="nav-link">Logout</a>
</li>
</ul>
{% else %}
<ul class="navbar-nav flex-row">
<li class="nav-item pe-3">
<a href="{{ url_for('auth.signup') }}" class="nav-link">Sign up</a>
</li>
<li class="nav-item pe-2">
<a href="{{ url_for('auth.login') }}" class="nav-link">Login</a>
</li>
</ul>
{% endif %}
</div>
</nav>
{% block content %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>
以下、signup.htmlとindex.htmlはbase.htmlを継承しています。
{% extends "auth/base.html" %}
{% block content %}
<div class="container">
<div class="card card-container">
<img id="profile-img" class="profile-img-card" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" />
<p id="profile-name" class="profile-name-card"></p>
<form action="{{ url_for('auth.signup', next=request.args.get(next)) }}" class="form-signin" method="POST" novalidate="novalidate">
{% for message in get_flashed_messages() %}
<p style="color: red";>{{ message }}</p>
{% endfor %}
{{ form.hidden_tag() }}
{% for error in form.username.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{{ form.username(class="form-control", placeholder="User name") }}
{% for error in form.email.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{{ form.email(class="form-control", placeholder="Email address") }}
{% for error in form.password.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{{ form.password(class="form-control", placeholder="Password") }}
{{ form.submit(class="btn btn-lg btn-primary btn-block btn-signin") }}
</form>
</div>
</div>
{% endblock %}
{% extends "auth/base.html" %}
{% block content %}
<h1>ユーザー認証完了ページ</h1>
{% endblock %}
BootStrap以外にも、style.cssを記述しています。
.card-container.card {
max-width: 350px;
padding: 40px 40px;
}
/*
* Card component
*/
.card {
background-color: #F7F7F7;
/* just in case there no content*/
padding: 20px 25px 30px;
margin: 0 auto 25px;
margin-top: 50px;
/* shadows and rounded borders */
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}
.profile-img-card {
width: 96px;
height: 96px;
margin: 0 auto 10px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
}
/*
* Form styles
*/
.profile-name-card {
font-size: 16px;
font-weight: bold;
text-align: center;
margin: 10px 0 0;
min-height: 1em;
}
.form-signin input,
.form-signin button {
width: 100%;
display: block;
margin-bottom: 10px;
z-index: 1;
position: relative;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.form-signin .form-control:focus {
border-color: rgb(104, 145, 162);
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162);
}
.btn.btn-signin,
.btn.btn-login {
background-color: rgb(104, 145, 162);
padding: 0px;
font-weight: 700;
font-size: 14px;
height: 36px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
border: none;
-o-transition: all 0.218s;
-moz-transition: all 0.218s;
-webkit-transition: all 0.218s;
transition: all 0.218s;
}
.btn.btn-signin:hover,
.btn.btn-signin:active,
.btn.btn-signin:focus {
background-color: #d63384;
}
.btn.btn-login:hover,
.btn.btn-login:active,
.btn.btn-login:focus {
background-color: #0d6efd;
}
動作確認
サインアップしていない(ログインしていない)状態だと、以下のように左上のAuthAppといったナビリンクを押下してもページ遷移できません。
また、何も入力していない状態でサインアップしてもバリデーションチェックによってエラーメッセージを表示しています。
サインアップ後は、ログイン状態になりログイン認証後のページに遷移しています。
ログイン機能の作成
ログイン機能作成における主な手順は以下になります。
- ログイン機能のフォームクラス作成
- ログイン機能のエンドポイント作成
- ログイン機能のテンプレート作成
- 動作確認
ログイン機能のフォームクラス作成
すでにサインアップ機能のフォームクラスを作成したため、forms.pyにログイン機能のコードを追加します。
# flask_wtfからFlaskFormクラスをインポート
from flask_wtf import FlaskForm
# wtformsから各フィールドをインポート
from wtforms import PasswordField, StringField, SubmitField
# wtformsから各バリデータをインポート
from wtforms.validators import DataRequired, Email, Length
# サインアップフォームクラス作成
class SignUpForm(FlaskForm):
username = StringField(
"ユーザー名",
validators = [
DataRequired("ユーザー名は必須です。"),
Length(1, 30, "30文字以内で入力してください。"),
],
)
email = StringField(
"メールアドレス",
validators = [
DataRequired("メールアドレスは必須です。"),
Email("メールアドレス形式で入力してください。"),
],
)
password = PasswordField(
"パスワード",
validators = [
DataRequired("パスワードは必須です。"),
],
)
submit = SubmitField("Sign up")
# ログインフォームクラス作成
class LoginForm(FlaskForm):
email = StringField(
"メールアドレス",
validators = [
DataRequired("メールアドレスは必須です。"),
Email("メールアドレス形式で入力してください。"),
],
)
password = PasswordField(
"パスワード",
validators = [
DataRequired("パスワードは必須です。"),
],
)
submit = SubmitField("Login")
- ログインフォームクラス作成
class LoginForm(FlaskForm):
email = StringField(
"メールアドレス",
validators = [
DataRequired("メールアドレスは必須です。"),
Email("メールアドレス形式で入力してください。"),
],
)
password = PasswordField(
"パスワード",
validators = [
DataRequired("パスワードは必須です。"),
],
)
submit = SubmitField("Login")
モデルの定義にてメールアドレスをユニークキーに指定してるため、メールアドレスとパスワードを入力してログイン可能にしています。
ログイン機能のエンドポイント作成
ログイン機能のエンドポイントを作成します。
from apps.app import db
from apps.auth.forms import SignUpForm, LoginForm
from apps.auth.models import User
from flask import Blueprint, render_template, flash, url_for, redirect, request
from flask_login import login_user, login_required
auth = Blueprint(
"auth",
__name__,
template_folder = "templates",
static_folder = "static",
)
@auth.route("/")
def index():
# TOPページへのアクセスはサインアップページに遷移させる
return redirect(url_for("auth.signup"))
@auth.route("/auth_ok")
@login_required
def auth_ok():
return render_template("index.html")
# signupエンドポイントを作成する
@auth.route("/signup", methods=["GET", "POST"])
def signup():
# SignUpFormをインスタンス化
signform = SignUpForm()
if signform.validate_on_submit():
user = User(
username = signform.username.data,
email = signform.email.data,
password = signform.password.data,
)
# メールアドレス重複チェック
if user.is_duplicate_email():
flash("指定のメールアドレスは登録済みです")
return redirect(url_for("auth.signup"))
# ユーザー情報を登録する
db.session.add(user)
db.session.commit()
# ユーザー情報をセッションに格納
login_user(user)
# GETパラメータにnextキーが存在し値がない場合にToDo一覧へ
next_ = request.args.get("next")
if next_ is None or not next_.startswith("/"):
next_ = url_for("auth.auth_ok")
return redirect(next_)
return render_template("auth/signup.html", form=signform)
# loginエンドポイントを作成する
@auth.route("/login", methods=["GET", "POST"])
def login():
loginform = LoginForm()
if loginform.validate_on_submit():
# メールアドレスからユーザー取得
user = User.query.filter_by(email=loginform.email.data).first()
# ユーザーが存在しパスワード一致ならログイン許可
if user is not None and user.verify_password(loginform.password.data):
login_user(user)
return redirect(url_for("auth.auth_ok"))
# ログイン失敗メッセージを設定する
flash("メールアドレスかパスワードが不正です")
return render_template("auth/login.html", form=loginform)
- LoginFormのインポート
- loginエンドポイント作成
- LoginFormのインスタンス作成
- メールアドレスからユーザー取得
- ユーザーが存在しパスワード一致ならログイン許可
- ログイン失敗メッセージを設定する
@auth.route("/login", methods=["GET", "POST"])
def login():
loginform = LoginForm()
if loginform.validate_on_submit():
# メールアドレスからユーザー取得
user = User.query.filter_by(email=loginform.email.data).first()
# ユーザーが存在しパスワード一致ならログイン許可
if user is not None and user.verify_password(loginform.password.data):
login_user(user)
return redirect(url_for("auth.auth_ok"))
# ログイン失敗メッセージを設定する
flash("メールアドレスかパスワードが不正です")
return render_template("auth/login.html", form=loginform)
デコレータにてloginエンドポイントを作成し、methodsにGETとPOSTを指定します。
login関数では、LoginFormクラスをインスタンス化しサブミット時のバリデートチェックを実施します。
サインアップと違い、すでに登録されたユーザー照合になるため、DBからユーザーを取得しています。
ユーザーは存在しており、かつパスワード一致すれば通過しログイン/リダイレクトされます。
ログイン機能のテンプレート作成
次に、ログイン機能を反映させたテンプレートを作成します。
flask-login
├── .env
└── apps
├── app.py
├── config.py
├── static
│ └── bootstrap.min.css
└── auth
├── __init__.py
├── forms.py
├── models.py
├── static/css
│ └── style.css
├── templates
│ └── auth
│ ├── base.html
│ ├── index.html
│ ├── login.html
│ └── signup.html
└── views.py
login.htmlを作成します。
{% extends "auth/base.html" %}
{% block content %}
<div class="container">
<div class="card card-container">
<img id="profile-img" class="profile-img-card" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" />
<p id="profile-name" class="profile-name-card"></p>
<form action="{{ url_for('auth.login') }}" class="form-signin" method="POST" novalidate="novalidate">
{% for message in get_flashed_messages() %}
<p style="color: red";>{{ message }}</p>
{% endfor %}
{{ form.hidden_tag() }}
{% for error in form.email.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{{ form.email(class="form-control", placeholder="Email address") }}
{% for error in form.password.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{{ form.password(class="form-control", placeholder="Password") }}
{{ form.submit(class="btn btn-lg btn-primary btn-block btn-login") }}
</form>
</div>
</div>
{% endblock %}
動作確認
サインアップ機能の動作確認でログイン状態になっているため、ブラウザをシークレットウィンドウで開き確認します。
右上のログインボタンを押下すると、以下のようにログイン認証ページに遷移します。
メールアドレスあるいはパスワードを間違えてログイン認証すると、以下のメッセージが表示されます。
ログアウト機能の作成
ログアウト機能作成における主な手順は以下になります。
- ログアウト機能のエンドポイント作成
- 動作確認
ログアウト機能のエンドポイント作成
ログアウト機能のエンドポイントを作成します。
from apps.app import db
from apps.auth.forms import SignUpForm, LoginForm
from apps.auth.models import User
from flask import Blueprint, render_template, flash, url_for, redirect, request
from flask_login import login_user, login_required, logout_user
auth = Blueprint(
"auth",
__name__,
template_folder = "templates",
static_folder = "static",
)
@auth.route("/")
def index():
# TOPページへのアクセスはサインアップページに遷移させる
return redirect(url_for("auth.signup"))
@auth.route("/auth_ok")
@login_required
def auth_ok():
return render_template("auth/index.html")
# signupエンドポイントを作成する
@auth.route("/signup", methods=["GET", "POST"])
def signup():
# SignUpFormをインスタンス化
signform = SignUpForm()
if signform.validate_on_submit():
user = User(
username = signform.username.data,
email = signform.email.data,
password = signform.password.data,
)
# メールアドレス重複チェック
if user.is_duplicate_email():
flash("指定のメールアドレスは登録済みです")
return redirect(url_for("auth.signup"))
# ユーザー情報を登録する
db.session.add(user)
db.session.commit()
# ユーザー情報をセッションに格納
login_user(user)
# GETパラメータにnextキーが存在し値がない場合にToDo一覧へ
next_ = request.args.get("next")
if next_ is None or not next_.startswith("/"):
next_ = url_for("auth.auth_ok")
return redirect(next_)
return render_template("auth/signup.html", form=signform)
# loginエンドポイントを作成する
@auth.route("/login", methods=["GET", "POST"])
def login():
loginform = LoginForm()
if loginform.validate_on_submit():
# メールアドレスからユーザー取得
user = User.query.filter_by(email=loginform.email.data).first()
# ユーザーが存在しパスワード一致ならログイン許可
if user is not None and user.verify_password(loginform.password.data):
login_user(user)
return redirect(url_for("auth.auth_ok"))
# ログイン失敗メッセージを設定する
flash("メールアドレスかパスワードが不正です")
return render_template("auth/login.html", form=loginform)
# logoutエンドポイントを作成する
@auth.route("/logout")
def logout():
logout_user()
return redirect(url_for("auth.login"))
- logout_userのインポート
- logoutエンドポイント作成
from flask_login import login_user, login_required, logout_user
flask_loginからlogout_userを追加インポートします。
@auth.route("/logout")
def logout():
logout_user()
return redirect(url_for("auth.login"))
デコレータにてlogoutエンドポイントを作成しています。
logout関数では、logout_user関数を呼び出すだけでログインセッションをリセットできます。
最終的にリダイレクトにてログイン画面にページ遷移します。
動作確認
以下はログイン認証後の画面になります。
右上のログアウトボタンを押下すると、ログイン認証画面にページ遷移します。
コメント