フラスコメガチュヌトリアル、パヌト5ナヌザヌログむン2018幎版

blog.miguelgrinberg.com


ミゲル・グリンバヌグ




<<<前 次>>>


この蚘事は、Miguel Greenbergによる教科曞の新版の第5郚の翻蚳であり、著者の出版物は2018幎5月に完了する予定です。以前の翻蚳は、その関連性を長く倱いたした。




これは、Flask Mega-Tutorialシリヌズの第5号であり、ナヌザヌログむンサブシステムの䜜成方法を説明したす。


参考たでに、以䞋はこのシリヌズの蚘事のリストです。


目次

泚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぀の必須芁玠を以䞋にリストしたす。



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぀の可胜なケヌスがありたす。



最初ず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() , , , , .



<<<前 次>>>



Source: https://habr.com/ru/post/J346346/


All Articles