Flaskの認証ライブラリ「flask-login」の「超」基本を解説します。
(筆者はFlaskのエキスパートではないので、細かい用語の間違いなどあってもご容赦ください。認識違いがあれば教えて下さい。)
はじめに
この記事で学べるトピック
- flask-loginの基礎を実際に動くコードで確認する
- flask-loginで、簡単な認証が動く
- 認証が必要なページにアクセスすると、まずログインページに遷移し、認証が成功すると本来のページを表示するとか、基本的な認証遷移
記事を執筆しようと思った動機(重要)
そもそも、何故この記事を書こうとしたか?ですが・・・
Flask素人の私がFlask認証を試したかったが、めちゃめちゃ苦労した
からです。
私はDjangoを利用した認証は何度かやってるのですが、それでもflask-loginは苦労しました。
いや、ネットに有用なサンプルが沢山あるんですよ。しかも、flask-loginの公式ドキュメントもかなりしっかりしている。
でも、flask-loginはアップデートが高頻度でなされており、公開されてるサンプルが動作しなかったり、(特に)認証以外のコンセプトとごっちゃになっていて認証以外の機能の理解が必要だったり苦労したのです。
そもそも、これは一般的な話ですが、Webシステムの認証って、考えてみると「色んな技術」の集大成なわけです。
- ユーザにWeb画面を公開するWebフレームワーク
- Webフレームワークを開発する言語(今回はPython)
- Webフレームワークのビューを効率よく開発するためのテンプレート
- 画面を表示するためのHTML/CSS
- 場合によってはユーザのブラウザでの挙動を動的に制御するためのJavaScript
- 画面間の遷移をコントロールしたりするQueryString
- ログイン情報を送信するフォーム
- バックエンドで認証情報を管理するDB
ざっと考えてみただけで、こんなにあるやん・・・
で、恐らくですが、ネット上の記事は「上記を例とするWeb開発周りの技術を広く浅く理解している」事が前提のものが殆どなのです。
更に、上記が前提とされているので「本来flask-loginとは直接関係のないロジック」とかもサンプルに入ってあったりして、flask-login(というか認証)以外の部分との切り分け・理解に時間がかかってしまう。
例えば殆どのサンプルでは、flask-loginと認証用のDBを合わせて説明されています。セキュリティの観点からは当然なのですが、flask-login自体は認証用DBがなくても動作します(ログインフレームワークなので当然っちゃ当然ですが)。
サンプルによってはflask-login以外の機能のコードが古くて、flask-loginそのものが動かないとかもあり・・・
なので「まずはflask-loginの認証以外は全て切り捨てたサンプル」ってのが、Flaskの認証の基本を確認する上で重要だと感じました。
とりあえず、動くサンプル
とにかく、動くサンプルです。動くサンプルさえあれば、説明がしやすい。
今回「DBも使わない、テンプレートも最小限の、マジでflask-loginに絞ったサンプル」をコンテナ化しました。layout.htmlとかも使用しない、本当に必要最低限のサンプルです。
以下のDockerfileとdocker-compose.yamlを同じフォルダに入れて「docker-compose up -d –build」してください。事前にDockerとdocker-composeは入れといてください。
Docker imageは元々は自分の開発環境を使っているので、そんなに中身がキレイじゃないです。ご了承ください。
Dockerfile:
FROM ccietozai/flask-login-basic
USER root
CMD ["/apps/start_server.sh"]
docker-compose.yml
version: '3'
services:
flask-login-basic:
restart: always
build: .
container_name: 'flask-login-basic'
working_dir: '/root/'
tty: true
ports:
- "5000:5000"
flask・flask-loginが必要なライブラリもPythonモジュールもサンプルアプリもポート開放も全て上記のコンテナ(とdocker-compose.yml)に入ってます。「docker-compose up -d –build」したら動くはず。
Dockerさえ動く環境があればいい!利用者の環境の違いとかのトラシューが不要!Docker最高!!
念の為、「docker ps」で動作を確認しましょう。
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7c1173da98d0 flask_login_docker_flask-login-basic "/apps/start_server.…" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp flask-login-basic
Flaskは5000/tcpで待ち受けており、ホストも同じく5000/tcpにポートフォワードしているので、サンプルは「DockerホストIP:5000」でアクセスできます(同じネットワーク)。
サンプルでは以下のページを準備しました。
- “/”は、認証不要のページ
- “/login”は、ログインページ
- “/secret”は、認証必要のページ
- “/logout”は、ログアウト処理(ログインページに自動遷移)
実際にやってみましょう。
まずは「http://dockerホストIP:5000/」でアクセスします。以下例ではIPが192.168.1.90になっていますが、これは私のDockerホストIPです。
こんな感じで、認証する前から普通にページが表示されます。認証不要のページと定義しているので。
次に「http://dockerホストIP:5000/login/」でアクセスします。
こんな感じで、ログインページが表示されます。
次に「http://dockerホストIP:5000/secret/」にアクセスします。すると、まだ認証されていないのでログインページに飛ばされます。この時のブラウザに表示されるURLは、単純に「http://dockerホストIP:5000/login/」を叩いたときと違っており、
こんな感じのパラメーターが追加されています。
この動作が「ログイン後に元のページに遷移する」為のポイントとなるのですが、後述します。
さて、表示されたログインページで、ユーザ名「user01」、パスワード「password」を入力し、「submit」を押下します。
無事、認証が必要なページが表示されました。
さて、ログアウトするために「http://dockerホストIP:5000/logout/」を入力します。本来であればログアウト用のhrefリンクを置いておくべきですが、今回は「flask-loginを学ぶ上で重要なポイント以外は全て削ぎ落とした」ため、このようにやります。
すると、無事、ログインページに遷移しました。
この状態はログインしていない状態なので、再度/secretにアクセスしてもログインページにリダイレクトされます。
以上で「最もシンプルな認証機能の画面遷移」が終了です。
内部のコードは「docker exec -it flask-login-basic bash」でコンテナにログインして好きに参照することが可能です。/appsの下に全て配置しています。
主要な機能についてはもう少し解説します。
flask-loginがやってること
私のサンプルコードは/apps/app.pyが全てです。それ以外にも/apps/templatesにhtmlの元がありますが、ロジックはapp.pyに簡単にまとめています。
この章では、その中でflask-loginで特に理解が必要なポイントを特筆してみます。
ログイン(認証のチェック)について
例えば「ログインフォームから取得したユーザ情報(今回はuseridとpassword)を元に認証テーブルと照会して、ユーザが存在するかの確認」などはflask-loginの仕事ではありません。
- ログインフォームの準備
- ログインフォームに入れたユーザ情報をFlaskへ送信(HTTP POST)
- ユーザ情報の受信
- ユーザ情報を認証リポジトリ(DBとか)への確認・突き合わせ
- 「4」の可否をもって、Flask内でユーザが認証されたものとする
上記がざっくりとした認証の流れですが、flask-loginが担っているのは「5」だけです。それが「login_post()」の中の以下コードです。
login_user(users.get(user_check[request.form["username"]]["id"]))
今回は以下のリンクの情報を参考に、dict(user_check)にベタでユーザ情報を入れました。
以下のサイトは本当に有用でした。ぜひ参考にするべし!
ユーザをログインさせ、ログインユーザIDを代入しています。
ちなみに、その前に以下で本当にユーザが登録されているかをチェックしていますが、これもflask-loginとは本質的には関係ありません。ただpythonでユーザの存在チェックをしているだけです。
if(request.form["username"] in user_check and request.form["password"] == user_check[request.form["username"]]["password"]):
なので、上のif文を外して、「誰でもログインした状態」にすることも可能です。
ログインフォームの準備(templateを作ったり)とか、認証をDBで構築するかとか、その辺はflask-loginにとっては「どうだって良い」わけで、login_userで単純ログイン状態にするイメージ。(「単純に」と言っても、ユーザセッションを制御したり、二重ログインを防ぐなどの処理を解釈したりと、色々自前で開発する必要がないのは非常に便利。java/jspでゴリゴリ設計していた過去とは大違いです)
ログアウト処理について
こちらは「logout_user()」が実行される事によって行われます。私のサンプルでは「logout()」配下にて実行されていますが、これはどこでも良いです。
ちなみに、logout_user()時にload_user()も呼ばれるようです。
ログインページの指定について
以下によって行われています。
login_manager.login_view = 'get_login'
これ、地味に重要です。内部の挙動で「XXの時にログインページに遷移」という処理が発生した時に使用されます。
例えば「まだログインされていない場合、ログインページに遷移」みたいな時に使われます。
これが次のトピックです。
ログインしてない場合にログインページに飛んで、承認されたら「戻る」
一般的なWebサイトの挙動ですね。
- ユーザが、ログインしていない状態で、ログインが必要なページにアクセスする
(私のサンプルでは/secretにアクセス) - ログインされていないので、ログインページに自動的に遷移(リダイレクト)
(私のサンプルでは/loginにリダイレクト) - ログインした後、自動的に元の「アクセスしたかったページ」に遷移
(私のサンプルでは/secretにリダイレクト)
こんな感じの画面遷移がflask-loginを使用すると簡単に実装できます。この辺からようやくflask-loginの便利さが肌で感じるようになります。
まず、ログインしたいページのview関係のrouteに「@login_required」を付与します。
@app.route('/secret',methods=['GET'])
@login_required
def go_secret():
return render_template("secret.html")
すると、認証されていないので、ログインページに遷移します。これを「@login_required」1行だけで実装できます。というのは様々なページで紹介されている通りです。
これ、内部的に何をやっているかというと。
- ユーザが/secretにGETしてきた
- 「@login_required」があるので、認証済かをチェックする判断
- 認証済じゃないので、ログインページにリダイレクト
上記の「2」と「3」をflask-loginがやってくれてるのです。本来だったらユーザのセッション管理とか、色々ややこしいことをflask-loginがやってくれているのです。
で、「3」のログインページにリダイレクトの箇所で、どれがログインページかを指定しているのが前述した「login_manager.login_view」となるのです。
ここまでOKでしょうか。
上記の挙動がわかってくると、次に以下の疑問が浮かび上がります。そもそも、画面遷移の挙動は以下となります(以下を実現させたい)。
- /secretから
- /loginから
- /secretに
「2」のタイミングで、/secretに戻りたい。「2」の/login状態で、次に遷移する情報「/secret」はどのように保持しているのか?
これ、実は、/secretにアクセス(HTTP GET)した後の/loginにリダイレクトしたタイミングで、Query Stringに「next」パラメータとして含まれています。
つまり
- ユーザが/secretにアクセス(HTTP GET)
- 「@login_required」があるので、/loginにリダイレクト(このタイミングで/secretをnextパラメータとしてQuery Stringに保存)
- ユーザが/loginのフォームを送信(HTTP POST)。この中にnextパラメータも入ってる。
- ログインチェックを行い、nextの中のURLのメソッドを実行
(私のサンプルではgo_secret)
てな感じの動作です。だから、序盤の「/secretにアクセスした後リダイレクトされた/loginのURL」を見ると。
nextに「/secret」が入ってますね。これを利用して最終遷移先を決定しているのです。
ちなみに、このnextですが、ユーザが入力するフォームのhtmlのformのactionを指定していたら消えます。何を言っているかというと、login.htmlが以下だとダメだということです。
<form method="POST" class="form-group" action="/login">
これをやると、action(送信先URL)が/loginだけで上書きされてしまうので、肝心のnextがなくなります。なので、nextを使いたかったらactionを指定してはいけません。
<form method="POST" class="form-group">
これだけで良いです。これで「login_manager.login_view」に指定したログインメソッドが呼ばれますし、nextが「request.args」に入ってくれるので、後はlogin_post()の中でnextを使ったリダイレクト処理を書くだけです。
next = request.args.get('next')
return redirect(next or '/')
どこからもリダイレクトされず、直接/loginが叩かれる可能性もあり、その場合は「request.args.get(‘next’)はNoneが返ります。その場合は(ログインが成功した後に)どこかに遷移させて上げる必要があるので、上記の例だと’/’に飛ばすようにしています。
まとめ
今回のように「敢えて、必要最低限の構成でのサンプル」があったほうが、対象となるコンポーネントの範囲や機能が明確になると思い、紹介しました。
htmlファイルも「login.html」以外は本当にベタなHTMLで作りました。
あとは認証リポジトリをDBにしたり、templateをキレイにしたり、CSSでデザインを調整したり、ロジックを分割したりと、supplementな実装を行えば良いわけです。
コメント