目次
はじめに
Flaskでログイン機能を実装し、ログインしていることをわかりやすくするためにユーザー名を表示したいと思ったのですが、全てのページに同じ内容(今回はユーザー名)を表示する際、簡単な方法が無いか調べてみました。
結論に至るまでの試行錯誤も余談として記録しようと思います。
結論
ログイン時にsessionにユーザー名を格納し、base.html等全ページに適用されるhtmlで表示させます。
環境
ソフトウェア | バージョン |
---|---|
Python | 3.9.0 |
Flask | 2.2.2 |
Flask-Login | 0.6.2 |
ディレクトリ構成
flask
├ flask_app
│ ├ models
│ ├ templates
│ │ ├ base.html
│ ├ └ ...
│ ├ views
│ │ └ auth.py
│ ├ __init__.py
│ └ ...
└ tests
Flaskのflashの内容をpytestでテストすると同様です。今回はviewフォルダ内のauth.py、templlatesフォルダ内のbase.htmlを使用します。
ログイン時の設定
from flask import Blueprint, render_template, request, \
flash, url_for, redirect,session
from flask_login import login_user
from werkzeug.security import check_password_hash
from sksk_app.models.questions import User
auth = Blueprint('auth', __name__, url_prefix='/auth')
@auth.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
email = request.form['email']
password = request.form['password']
user = User.query.filter_by(email=email).first()
if not user or not check_password_hash(user.password, password):
flash('パスワードが異なります')
return redirect(url_for('auth.login'))
flash('ログインしました')
login_user(user)
session['user_id'] = user.id
session['user_name'] = user.name
return redirect(url_for('pg.toppage'))
page_title = 'ログイン'
return render_template('auth/login.html',page_title=page_title)
実際のコードではなく、importなどは今回の設定に必要なものだけに絞っています。
ログインの仕組みについては本記事では割愛します。
こちらのログイン関係のコードは Flask-Login を使用してアプリケーションに認証を追加する方法 | DigitalOcean を大いに参考にしています。
sessionへのユーザー名の格納
login_user(user)
session['user_id'] = user.id
session['user_name'] = user.name
「login_user」メソッドでログインした後に、sessionにそれぞれ値を格納します。
session is a dict that stores data across requests. When validation succeeds, the user’s id is stored in a new session. The data is stored in a cookie that is sent to the browser, and the browser then sends it back with subsequent requests. Flask securely signs the data so that it can’t be tampered with.
Login - Blueprints and Views — Flask Documentation (2.2.x)
「session['user_name']」に、ログイン時に表示したい名前を格納します。
全ページに表示する
1つのコードで全てのページに表示させるには、base.htmlという全てのページに共通するファイルを用意し、それぞれのページで読み込みます。
Each page in the application will have the same basic layout around a different body. Instead of writing the entire HTML structure in each template, each template will extend a base template and override specific sections.
Templates — Flask Documentation (2.2.x)
このbase.htmlに「session['user_name']」を表示させれば良いだけです。
注: Web上の記事や書籍では、このBase Layoutのbase.htmlをlayout.htmlとするところも多いです。私は公式チュートリアルに沿ってbase.htmlにしています。
base.html
表示させたいところに「{{session['user_name']}}」を記述します。
下記の例ではpタグの中に表示、続けて「さん」を表示させる様にします。
<p> {{session['user_name']}} さん</p>
これで全ページにユーザー名を表示することができます。
余談 render_templateとg
render_templateで全ページに設定?
ページ間で値をやりとりするとしたらsessionを使用するのは何かの機会に知りました。
また、画面に変数の値を表示する方法といえば、render_template()でHTMLを生成する際に変数を設定する方法しか知りませんでした。
ということで、render_template()とsessionを組み合わせるのかなあと思ったのですが、そのやり方でやると、viewの全ての関数でsessionから情報取得→render_templateで値の受け渡し、という膨大なコードが生まれてしまいます。
世の中のアプリがこんな面倒なことをしているとは思えないので、何か良い策は無いかなあと思っていました。
sessionもbase.htmlによるhtml生成も知っていたのに、なぜか二つを組み合わせることが思いつきませんでした。この方法を教えてくれた以下のStacOverflowの解答に感謝です。
公式チュートリアルのやり方 Flask.g
公式ではFlask.gというものを使用して、ユーザー名の表示をしています。
{% if g.user %}
<li><span>{{ g.user['username'] }}</span>
<li><a href="{{ url_for('auth.logout') }}">Log Out</a>
{% else %}
<li><a href="{{ url_for('auth.register') }}">Register</a>
<li><a href="{{ url_for('auth.login') }}">Log In</a>
{% endif %}
Templates — Flask Documentation (2.2.x)より一部抜粋
この「g」とはなんでしょうか?
アプリケーション・コンテクストを通じてデータを格納できる場所とのことです。
A namespace object that can store data during an application context. This is an instance of Flask.app_ctx_globals_class, which defaults to ctx._AppCtxGlobals.
This is a good place to store resources during a request.
flask.g - API — Flask Documentation (2.2.x)
ではアプリケーション・コンテクストとは?
アプリケーションレベルのデータをリクエストの間に記録しておくものです。
The application context keeps track of the application-level data during a request, CLI command, or other activity. Rather than passing the application around to each function, the current_app and g proxies are accessed instead.
The Application Context — Flask Documentation (2.2.x)
なんだか良さそうなものの気もしますが、sessionが「複数のリクエストにまたがって」(session is a dict that stores data across requests.)データを格納しておくのに対し、アプリケーション・コンテクストは「1つのリクエスト・CLIコマンド・その他のアクティビティの間」(The application context keeps track of the application-level data during a request, CLI command, or other activity.)データを記録するもの、とあります。
従って、リクエストが新たに作られる度にgにデータを格納する必要があります。
公式チュートリアルのアプリではgを利用するために、以下のようなコードがあります。
@bp.before_app_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = get_db().execute(
'SELECT * FROM user WHERE id = ?', (user_id,)
).fetchone()
bp.before_app_request() registers a function that runs before the view function, no matter what URL is requested.
Login - Blueprints and Views — Flask Documentation (2.2.x)
before_app_request()で、View機能が実行される前に、sessionからユーザーのidを取得し、そこからDBからユーザー情報を取得、gに格納、という流れです。
before_app_request()はbefore_request()のようなものだけど、Blueprintで扱うものに対して実行されるようです。
Like before_request(), but before every request, not only those handled by the blueprint. Equivalent to Flask.before_request().
before_app_request(f) - API — Flask Documentation (2.2.x)
before_request()はBlueprintを適用していないものに対し、各リクエストの前に機能を登録して実行するものです。
Register a function to run before each request.
For example, this can be used to open a database connection, or to load the logged in user from the session.
before_request(f) - API — Flask Documentation (2.2.x)
おそらくリクエスト毎にこの処理を行う目的として、本当にそのユーザーかどうかの確認をするものだと思うのですが、1つのリクエスト毎にDBから情報を取得することが疑問だったので、とりあえずこの処理は省くことにしました。
そもそもgを実装したのは、公式チュートリアルのテストを実行する際に、gから情報を取得することもテスト項目に入っていたからでした。とりあえず今のところはテストも含め、gは利用せず、sessionでユーザー名の表示をしておこうと思います。