ミゲル・グリンバーグ
この記事は、Miguel Greenbergによる教科書の新版の第5部の翻訳であり、著者の出版物は2018年5月に完了する予定です。以前の翻訳は、その関連性を長く失いました。
これは、Flask Mega-Tutorialシリーズの第5号であり、ユーザーログインサブシステムの作成方法を説明します。
参考までに、以下はこのシリーズの記事のリストです。
- 第1章:Hello world!
- 第2章:テンプレート
- 第3章:Webフォーム
- 第4章:データベース
- 第5章:ユーザーログイン(この記事)
- 第6章:プロフィールページとアバター
- 第7章:エラー処理
- 第8章:サブスクライバー、連絡先、およびフレンド
- 第9章:ページネーション
- 第10章:メールサポート
- 第11章:再構成
- 第12章:日付と時刻
- 第13章:I18nおよびL10n
- 第14章:Ajax
- 第15章:アプリケーション構造の改善
- 第16章:全文検索
- 第17章:Linuxの展開
- 第18章:Herokuでの展開
- 第19章:Dockerコンテナーでの展開
- 第20章:JavaScriptマジック
- 第21章:ユーザー通知
- 第22章:バックグラウンドタスク
- 第23章:アプリケーションプログラミングインターフェイス(API)
注1:このコースの古いバージョンをお探しの場合は、こちらをご覧ください 。
注2:突然、このブログの私の(ミゲル)の仕事を支持して声を上げたい場合、または単に記事を1週間待つ忍耐がない場合、私(ミゲルグリーンバーグ)はこのガイドの完全版にパッケージ化された電子書籍またはビデオを提供します。 詳細については、 learn.miguelgrinberg.comをご覧ください 。
第3章では 、ユーザーログインフォームの作成方法を学び、 第4章では、データベースの操作方法を学びました。 この章では、これら2つの章のトピックを組み合わせて簡単なユーザーログインシステムを作成する方法を学習します。
この章のGitHubリンク: Browse 、 Zip 、 Diff 。
パスワードハッシュ
ユーザーモデルの第4章では 、 password_hash
フィールドが割り当てられましたが、まだ使用されていません。 このフィールドの目的は、ユーザーのパスワードハッシュを保存することです。これは、登録プロセス中にユーザーが入力したパスワードの検証に使用されます。 パスワードハッシュはセキュリティの専門家に任せるべき複雑なトピックですが、アプリケーションから呼び出せるようにこのロジックをすべて実装する使いやすいライブラリがいくつかあります。
パスワードハッシュを実装するパッケージの1つにWerkzeugがあります。これは、Flaskのインストール時にpipの出力で確認できた可能性があります。 これは依存関係であるため、Werkzeugは仮想環境に既にインストールされています。 次のPythonシェルセッションは、パスワードをハッシュする方法を示しています。
>>> from werkzeug.security import generate_password_hash >>> hash = generate_password_hash('foobar') >>> hash 'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f'
この例では、foobarパスワードは、逆演算を行わない一連の暗号化演算によって長いコード化された文字列に変換されます。つまり、ハッシュされたパスワードを受け取った人は、それを使用して元のパスワードを取得できません。 追加の手段として、同じパスワードを複数回使用すると、異なる結果が得られるため、2人のユーザーがハッシュを見て同じパスワードを持っているかどうかを判断することはできません。
検証プロセスは、次のようにWerkzeugの2番目の関数を使用して実行されます。
>>> from werkzeug.security import check_password_hash >>> check_password_hash(hash, 'foobar') True >>> check_password_hash(hash, 'barfoo') False
検証機能は、以前に生成されたパスワードと、ログイン中にユーザーが入力したパスワードのハッシュを受け入れます。 この関数は、ユーザーが指定したパスワードがハッシュと一致する場合はTrue
、そうでない場合はFalse
返します。
すべてのパスワードハッシュロジックは、ユーザーモデルの2つの新しいメソッドとして実装できます。
app/models.py
:ハッシュとパスワード検証
from werkzeug.security import generate_password_hash, check_password_hash # ... class User(db.Model): # ... def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)
これら2つの方法を使用すると、ユーザーオブジェクトは元のパスワードを保存することなく、安全なパスワードチェックを実行できるようになりました。 これらの新しいメソッドの使用例を次に示します。
>>> u = User(username='susan', email='susan@example.com') >>> u.set_password('mypassword') >>> u.check_password('anotherpassword') False >>> u.check_password('mypassword') True
Flask-Loginの概要
この章では、Flask-Loginと呼ばれる非常に人気のあるFlask拡張機能を紹介します。 この拡張機能は、ユーザーのログイン状態を制御し、たとえば、ユーザーがアプリケーションにログインし、アプリケーションがユーザーのログインを「記憶」するまで別のページに移動できるようにします。 また、Remember Me機能を提供します。これにより、ユーザーはブラウザーウィンドウを閉じた後でもログインしたままにできます。 この章の準備をするには、仮想環境にFlask-Loginをインストールすることから始めます。
(venv) $ pip install flask-login
他の拡張機能と同様、Flask-Loginはapp/__init__.py
アプリケーションインスタンスの直後に作成および初期化する必要があります。 したがって、この拡張機能は初期化されます。
app/__init__.py
初期化
# ... from flask_login import LoginManager app = Flask(__name__) # ... login = LoginManager(app) # ...
Flask-Login用のユーザーモデルの準備
Flask-Login拡張機能は、カスタムアプリケーションモデルで動作し、特定のプロパティとメソッドがそれに実装されることを想定しています。 このアプローチは、これらの必要な要素がモデルに追加される限り、Flask-Loginには他の要件がないため、たとえば、データベースシステムに基づいたユーザーモデルで動作できるという点で優れています。
4つの必須要素を以下にリストします。
-
is_authenticated
:ユーザーに有効な資格情報がある場合はTrue
、そうでない場合はFalse
なるプロパティ。 -
is_active
:ユーザーアカウントがアクティブな場合はTrue
を返し、そうでない場合はFalse
を返すプロパティ。 -
is_anonymous
:通常のユーザーの場合はFalse
を返し、ユーザーが匿名の場合はTrue
を返すプロパティ。 -
get_id()
:ユーザーの一意の識別子を文字列として返すメソッド(Python 2を使用している場合はユニコード)。
4つすべてを簡単に実装できますが、実装はかなり一般的であるため、Flask-Loginはユーザークラスのほとんどのクラスに適した一般的な実装を含むミックスインクラスUserMixin
提供します。 これは、 ミックスインクラスがモデルに追加される方法です。
app/models.py
ユーザーミックスインクラス
# ... from flask_login import UserMixin class User(UserMixin, db.Model): # ...
カスタムローダー
Flask-Loginは、アプリケーションに接続する各ユーザーに割り当てられたFlask ユーザーセッションに一意の識別子を保存することにより、登録ユーザーを追跡します。 ログオンしているユーザーが新しいページにアクセスするたびに、Flask-LoginはセッションからユーザーIDを取得し、そのユーザーをメモリにロードします。
Flask-Loginはデータベースについて何も知らないため、ユーザーをロードするときにアプリケーションの助けが必要です。 このため、拡張機能は、アプリケーションがユーザーブートローダー機能を設定することを想定しています。この機能は、ユーザーに識別子をロードするために呼び出すことができます。 この関数はapp / models.pyモジュールに追加できます :
app/models.py
ユーザーローダー機能
from app import login # ... @login.user_loader def load_user(id): return User.query.get(int(id))
ユーザーブートローダーは、 @login.user_loader
デコレーターを使用してFlask-Loginに登録されます。 Flask-Loginが引数として関数に渡す識別子は文字列になります。そのため、数値識別子を使用するデータベースでは、上記のint(id)
ように、文字列を整数に変換する必要があります。
ユーザーログイン
ログイン機能に移りましょう。これは、思い出すように、 flash()
メッセージのみを発行する偽のログインを実装しました。 これで、アプリケーションはユーザーデータベースにアクセスし、パスワードハッシュを作成および確認する方法を知ったので、このブラウジング機能を完了することができます( \microblog\app\routes.py
)。
app/routes.py
:ログインビュー関数のロジック
# ... from flask_login import current_user, login_user from app.models import User # ... @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('index')) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first() if user is None or not user.check_password(form.password.data): flash('Invalid username or password') return redirect(url_for('login')) login_user(user, remember=form.remember_me.data) return redirect(url_for('index')) return render_template('login.html', title='Sign In', form=form)
login()
関数の最初の2行は、奇妙な状況につながりlogin()
。 ログインしていて、アプリケーションの/login
URLにアクセスしたいユーザーがいると想像すると、認証ページにリダイレクトされます。 明らかに、これは間違いなので、これを許可したくありません。 current_user
変数はFlask-Loginから取得され、いつでもユーザーオブジェクトを取得するために使用できます。 この変数の値は、データベースのユーザーオブジェクト(Flask-Loginが上記のユーザーローダーコールバックを介して読み取る)、またはユーザーがまだログインしていない場合は特別な匿名ユーザーオブジェクトです。 Flaskがユーザーオブジェクトで必要とするプロパティを覚えていますか? それらの1つはis_authenticated
。これは、ユーザーが登録されているかどうかを確認するのに非常に便利です。 ユーザーが既にログインしている場合は、単にインデックスページにリダイレクトします。
以前に使用したflash()を呼び出す代わりに、実際にユーザーのシステムにログインできます。 最初のステップは、データベースからユーザーをロードすることです。 ユーザー名は送信フォームに付属しているため、データベースを照会してユーザーを見つけることができます。
これを行うには、SQLAlchemyクエリオブジェクトのfilter_by()
メソッドを使用します。 filter_by()
の結果は、一致するユーザー名を持つオブジェクトのみを含むクエリです。 結果が1つまたは0になることを知っているので、 first()
呼び出して要求を終了します。これは、存在する場合はユーザーオブジェクトを返し、存在しない場合はNone
を返します。 第4章では、クエリでall()
メソッドを呼び出すと、クエリが実行され、そのクエリに一致するすべての結果のリストを取得することがわかりました。 first()
メソッドは、1つの結果のみが必要な場合にクエリを実行する別の方法です。
提供されたユーザー名と一致する場合、フォームに付属しているパスワードが有効かどうかを確認できます。 これは、上記で定義したcheck_password()
メソッドを呼び出すことで実行されます。 これにより、ユーザーがハッシュパスワードを保存し、フォームに入力したパスワードがハッシュと一致するかどうかを判断します。 そのため、2つのエラー条件が考えられます。ユーザー名が無効であるか、ユーザーのパスワードが正しくない可能性があります。 いずれの場合も、ユーザーが再試行できるように、メッセージをスクロールしてログインプロンプトにリダイレクトします。
ユーザー名とパスワードが正しい場合、Flask-Loginからlogin_user()
関数を呼び出します。 この関数はログイン時にユーザーを登録するため、このユーザーのcurrent_user
変数は、ユーザーがcurrent_user
する将来のページで設定されます。
ログインプロセスを完了するには、新しく登録したユーザーをインデックスページにリダイレクトするだけです。
ログアウト
明らかに、アプリケーションを終了するオプションをユーザーに提供する必要があります。 これは、Flask-Loginのlogout_user()
関数を使用してlogout_user()
できます。 exit関数は次のようになります。
app/routes.py
:ログアウトビュー機能
# ... from flask_login import logout_user # ... @app.route('/logout') def logout(): logout_user() return redirect(url_for('index'))
ユーザーがログインした後、ナビゲーションバーのlogin
リンクを自動的にlogout
リンクに切り替えることができます。 これは、 base.htmlテンプレートの条件式を使用して実行できます。
app/templates/base.html
:条件付きログインおよびログアウトリンク
<div> Microblog: <a href="{{ url_for('index') }}">Home</a> {% if current_user.is_anonymous %} <a href="{{ url_for('login') }}">Login</a> {% else %} <a href="{{ url_for('logout') }}">Logout</a> {% endif %} </div>
is_anonymous
プロパティは、Flask-LoginがUserMixinクラスを通じてユーザーオブジェクトに追加する属性の1つです。 式current_user.is_anonymous
は、ユーザーがログインしていない場合にのみTrue
を返します。
ユーザーログイン要件
Flask-Loginは、アプリケーションの特定のページを表示する前にユーザーに登録を強制する非常に便利な機能を提供します。 ログインしていないユーザーが安全なページを表示しようとすると、Flask-Loginは自動的にユーザーをログインフォームにリダイレクトします。ログインプロセスが完了すると、ユーザーが表示したいページにリダイレクトされます。
この関数を実装するには、Flask-Loginはログインを処理するビュー関数が何であるかを知っている必要があります。 これはapp / init .pyに追加できます:
# ... login = LoginManager(app) login.login_view = 'login'
上記の「ログイン」値は、ログインするための関数(またはエンドポイント)の名前です。 つまり、URLを取得するためにurl_for()
呼び出しで使用する名前。
Flask-Loginメソッドは、 @login_required
というデコレーターを使用して、匿名ユーザーからブラウジング機能を保護します。 Flaskの@app.route
デコレーターの下のビュー関数にこのデコレーターを追加すると、関数は保護され、認証されていないユーザーへのアクセスが許可されなくなります。 デコレータをアプリケーションインデックスビュー関数に適用する方法は次のとおりです。
app/routes.py
デコレーター
from flask_login import login_required @app.route('/') @app.route('/index') @login_required def index(): # ...
ログインの成功から、ユーザーがアクセスしたいページへのリダイレクトを実装することは残っています。 ログインしていないユーザーが@login_required
デコーダーによって保護されているブラウジング機能にアクセスすると、デコレーターはログインページにリダイレクトしようとしますが、アプリケーションが最初のページに戻ることができるように、このリダイレクトに追加情報が含まれます。 たとえば、ユーザーが/ indexに移動すると、 @login_required
ハンドラー@login_required
リクエスト@login_required
インターセプトし、 /login
へのリダイレクトで応答しますが、このURLにクエリ文字列引数を追加して、完全なURL / login?Next = / indexを作成します 。 クエリ文字列next
引数はソースURLに設定されるため、アプリケーションはこれを使用してログイン後にリダイレクトできます。
次に、 next
クエリ文字列引数を読み取って処理する方法を示すコードスニペットを示します。
app/routes.py
:「次の」ページにリダイレクトします
from flask import request from werkzeug.urls import url_parse @app.route('/login', methods=['GET', 'POST']) def login(): # ... if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first() if user is None or not user.check_password(form.password.data): flash('Invalid username or password') return redirect(url_for('login')) login_user(user, remember=form.remember_me.data) next_page = request.args.get('next') if not next_page or url_parse(next_page).netloc != '': next_page = url_for('index') return redirect(next_page) # ...
Flask-Loginからlogin_user()
関数を呼び出してユーザーがログインした直後に、クエリ文字列のnext
引数の値を受け取ります。 Flaskには、クライアントがリクエストとともに送信したすべての情報を含むリクエスト変数が含まれています。 特に、 request.args
属性は、 request.args
辞書形式でクエリ文字列の内容を提供します。 実際に、ログインが成功した後にリダイレクトする場所を決定するために考慮する必要がある3つの可能なケースがあります。
- ログインURLに次の引数がない場合、ユーザーはインデックスページにリダイレクトされます。
- ログインURLに
next
引数が含まれており、これが相対パス(つまり、ドメインの一部を含まないURL)に設定されている場合、ユーザーはそのURLにリダイレクトされます。 - ログインURLに
next
引数が含まれていて、ドメイン名を含む完全なURLに設定されている場合、ユーザーはインデックスページにリダイレクトされます。
最初と2番目のケースでは、説明は不要です。 3番目のケースは、アプリケーションをより安全にすることです。 攻撃者は悪意のあるサイトへのURLをnext
引数に挿入できるため、アプリケーションはURLをリダイレクトするだけです。これにより、リダイレクトはアプリケーションと同じサイトに確実に残ります。 URLが相対か絶対かを判断するには、 Werkzeugの url_parse()
関数を使用して分析し、 netloc
コンポーネントがnetloc
かどうかを確認します。
ログインしたユーザーをテンプレートに表示する
第2章に戻って、ユーザーサブシステムが作成される前にアプリケーションのホームページを開発するために偽のユーザーを作成したことを覚えていますか? さて、アプリケーションには実際のユーザーがいるので、偽のユーザーを削除して、実際のユーザーとの作業を開始できます。 偽物の代わりに、テンプレートでFlask-Login-s current_userを使用できます。
app/templates/index.html
:現在のユーザーをテンプレートに渡します
{% extends "base.html" %} {% block content %} <h1>Hi, {{ current_user.username }}!</h1> {% for post in posts %} <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div> {% endfor %} {% endblock %}
そして、 view
関数でuser
引数を削除できます( microblog \ app \ routes.py ):
app / routes.py:ユーザーをテンプレートに渡さない
@app.route('/') @app.route('/index') def index(): # ... return render_template("index.html", title='Home Page', posts=posts)
入力と出力の操作性をチェックするのにふさわしい時が来たようです。 ユーザー登録がまだ不足しているため、データベースにユーザーを追加する唯一の方法は、Pythonシェルを介して行うことです。そのため、 flask shell
を実行し、次のコマンドを入力してユーザーを登録します。
>>> u = User(username='susan', email='susan@example.com') >>> u.set_password('cat') >>> db.session.add(u) >>> db.session.commit()
アプリケーションを実行してhttp:// localhost:5000/
またはhttp://localhost:5000/index
にアクセスしようとすると、すぐにログインページにリダイレクトされます。 そして、データベースに追加したユーザーの資格情報を使用して、ログイン手順を完了すると、パーソナライズされた挨拶が表示される元のページに戻ります。
ユーザー登録
この章で作成する機能の最後の部分は、ユーザーがWebフォームから登録できるようにするための登録フォームです。 app / forms.pyでWebフォームクラスを作成することから始めましょう。
from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import ValidationError, DataRequired, Email, EqualTo from app.models import User # ... class RegistrationForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()]) password2 = PasswordField( 'Repeat Password', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('Register') def validate_username(self, username): user = User.query.filter_by(username=username.data).first() if user is not None: raise ValidationError('Please use a different username.') def validate_email(self, email): user = User.query.filter_by(email=email.data).first() if user is not None: raise ValidationError('Please use a different email address.')
この新しいフォームには、検証に関連する興味深いことがいくつかあります。 まず、メールのメールフィールドにemail
DataRequiredの後にEmailという2番目のバリデーターを追加しました。 これは、WTFormsに付属する別のバリデーター(元の「ストックバリデーター」、つまり組み込みの標準として翻訳する方が正しい)です。これにより、ユーザーがこのフィールドに入力した内容がメールアドレスの構造と一致するようになります。
これは登録フォームであるため、タイプミスのリスクを軽減するために、ユーザーにパスワードを2回入力するように依頼するのが通常です。 このため、 password
とpassword2
がありpassword2
。 2番目のパスワードフィールドは、別の標準EqualToバリデータを使用します。これは、その値が最初のパスワードフィールドの値と同一であることを確認します。
また、このクラスにvalidate_username()
およびvalidate_email()
2つのメソッドを追加しました。 validate_<_>
パターンに一致するメソッドを追加すると、WTFormsはそれらをカスタムバリデーターとして受け入れ、標準バリデーターに加えてそれらを呼び出します。 この場合、ユーザーが入力したユーザー名と電子メールアドレスが既にデータベースにないことを確認したいので、これらの2つの方法はデータベースにクエリを発行し、結果がないことを期待します。 , , ValidationError . , , , .
-, HTML-, app/templates/register.html . , :
{% extends "base.html" %} {% block content %} <h1>Register</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.username.label }}<br> {{ form.username(size=32) }}<br> {% for error in form.username.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.email.label }}<br> {{ form.email(size=64) }}<br> {% for error in form.email.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.password.label }}<br> {{ form.password(size=32) }}<br> {% for error in form.password.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.password2.label }}<br> {{ form.password2(size=32) }}<br> {% for error in form.password2.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %}
, , :
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
, , , app/routes.py :
from app import db from app.forms import RegistrationForm # ... @app.route('/register', methods=['GET', 'POST']) def register(): if current_user.is_authenticated: return redirect(url_for('index')) form = RegistrationForm() if form.validate_on_submit(): user = User(username=form.username.data, email=form.email.data) user.set_password(form.password.data) db.session.add(user) db.session.commit() flash('Congratulations, you are now a registered user!') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form)
, . , . , if validate_on_submit()
, , , , .