20: 認証付きログイン(20: Logins with authentication)¶
ユーザのリストに対してユーザ名とパスワードを認証するログイン用のビュー。
Background¶
ほとんどのWebアプリケーションにはWebブラウザ経由でコンテンツを追加/編集/削除できるURLがあります。アプリケーションに security を追加する時です。こ最初のステップでログイン機能を導入します。プラグイン可能なユーザストレージにPyramidの豊富な機能を使用して、ログインしてログアウトします。
次のステップでは、認証セキュリティーステートメントによるリソースの保護について紹介します。
目的(Objectives)¶
- Pyramidの認証の概念を紹介します。
- ログイン、ログアウトのビューを作成します。
手順(Steps)¶
ビューのクラスを出発点として使用します
$ cd ..; cp -r view_classes authentication; cd authentication
依存関係として
authentication/setup.py
にbcrypt
を追加します:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
from setuptools import setup requires = [ 'bcrypt', 'pyramid', 'pyramid_chameleon', 'waitress', ] setup(name='tutorial', install_requires=requires, entry_points="""\ [paste.app_factory] main = tutorial:main """, )
プロジェクトを開発者モードでインストールできました:
$ $VENV/bin/pip install -e .
セキュリティハッシュをコードに入れるのではなく、設定ファイル
authentication/development.ini
のtutorial.secret
に記載します:1 2 3 4 5 6 7 8 9 10
[app:main] use = egg:tutorial pyramid.reload_templates = true pyramid.includes = pyramid_debugtoolbar tutorial.secret = 98zd [server:main] use = egg:waitress#main listen = localhost:6543
認証(および今のところ認証ポリシー)を取得して、ログインするには
authentication/tutorial/__init__.py
の configurator で以下のようにします:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from pyramid.config import Configurator from .security import groupfinder def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_chameleon') # Security policies authn_policy = AuthTktAuthenticationPolicy( settings['tutorial.secret'], callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) config.add_route('home', '/') config.add_route('hello', '/howdy') config.add_route('login', '/login') config.add_route('logout', '/logout') config.scan('.views') return config.make_wsgi_app()
「認証ポリシーコールバック」を提供することによって。ユーザー情報を見つけられる
authentication/tutorial/security.py
モジュールを作成します:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import bcrypt def hash_password(pw): pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) return pwhash.decode('utf8') def check_password(pw, hashed_pw): expected_hash = hashed_pw.encode('utf8') return bcrypt.checkpw(pw.encode('utf8'), expected_hash) USERS = {'editor': hash_password('editor'), 'viewer': hash_password('viewer')} GROUPS = {'editor': ['group:editors']} def groupfinder(userid, request): if userid in USERS: return GROUPS.get(userid, [])
authentication/tutorial/views.py
のビューを更新します:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
from pyramid.httpexceptions import HTTPFound from pyramid.security import ( remember, forget, ) from pyramid.view import ( view_config, view_defaults ) from .security import ( USERS, check_password ) @view_defaults(renderer='home.pt') class TutorialViews: def __init__(self, request): self.request = request self.logged_in = request.authenticated_userid @view_config(route_name='home') def home(self): return {'name': 'Home View'} @view_config(route_name='hello') def hello(self): return {'name': 'Hello View'} @view_config(route_name='login', renderer='login.pt') def login(self): request = self.request login_url = request.route_url('login') referrer = request.url if referrer == login_url: referrer = '/' # never use login form itself as came_from came_from = request.params.get('came_from', referrer) message = '' login = '' password = '' if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] hashed_pw = USERS.get(login) if hashed_pw and check_password(password, hashed_pw): headers = remember(request, login) return HTTPFound(location=came_from, headers=headers) message = 'Failed login' return dict( name='Login', message=message, url=request.application_url + '/login', came_from=came_from, login=login, password=password, ) @view_config(route_name='logout') def logout(self): request = self.request headers = forget(request) url = request.route_url('home') return HTTPFound(location=url, headers=headers)
ログイン用のテンプレート
authentication/tutorial/login.pt
を追加します:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
<!DOCTYPE html> <html lang="en"> <head> <title>Quick Tutorial: ${name}</title> </head> <body> <h1>Login</h1> <span tal:replace="message"/> <form action="${url}" method="post"> <input type="hidden" name="came_from" value="${came_from}"/> <label for="login">Username</label> <input type="text" id="login" name="login" value="${login}"/><br/> <label for="password">Password</label> <input type="password" id="password" name="password" value="${password}"/><br/> <input type="submit" name="form.submitted" value="Log In"/> </form> </body> </html>
ログイン、ログアウトのボックスを提供する``authentication/tutorial/home.pt``を追加します:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<!DOCTYPE html> <html lang="en"> <head> <title>Quick Tutorial: ${name}</title> </head> <body> <div> <a tal:condition="view.logged_in is None" href="${request.application_url}/login">Log In</a> <a tal:condition="view.logged_in is not None" href="${request.application_url}/logout">Logout</a> </div> <h1>Hi ${name}</h1> <p>Visit <a href="${request.route_url('hello')}">hello</a></p> </body> </html>
Pythonアプリケーションを以下のように実行します:
$ $VENV/bin/pserve development.ini --reload
http://localhost:6543/ をブラウザで開きます:
リンク「Log In」をクリックしてください。
Submit the login form with the username
editor
and the passwordeditor
.リンク「Log In」が「Logout」に変更されることに注意してください。
リンク「Logout」をクリックしてください。
分析(Analysis)¶
多くのWebフレームワークとは異なり、Pyramidには、ビルトインですが認証と認可用のオプションのセキュリティモデルがふくまれています。このセキュリティシステムは柔軟で多くのニーズをサポートするのが目的です。セキュリティのモデルでは、認証(あなたが誰か)と承認(何が許可されているか)は単なるプラグインではなく分離されています。一度に1つの手順を学ぶためにユーザーを識別してログアウトするシステムを提供しています
例では、バンドルされた AuthTktAuthenticationPolicy ポリシーを使用することを選択しました。設定で有効にして、INIファイルにチケット署名の秘密を提供しました。
ビューのクラスはログインビューを成長させました。 GET
リクエストで到達したときにログインフォームを返しました。 POST
経由で到達したとき、が設定に登録した 「groupfinder」は呼び出し可能なファイルに対して、送信されたユーザー名とパスワードを処理しました。
hash_password
関数はパスワードをプレーンテキストで保存するのではなく、 bcrypt
をかいしてユーザーのパスワードに 一方向のSALTハッシュアルゴリズムを使用します。これはセキュリティの「ベストプラクティス」とみなされます。
注釈
システム上で問題がある場合は、bcrypt
の代替ライブラリがあります。ライブラリが安全にパスワードを格納するために承認されたアルゴリズムを使用していることを確認してください。
check_password
関数はサブミットされたパスワードとデータベースに格納されているユーザーのパスワードの2つのハッシュ値を比較します。ハッシュされた値が同等である場合は認証されます。そうでない場合は認証は失敗します。
テンプレートでは、ビュークラスから logged_in
の値を取得しました。これを使用してログインしているユーザーがあれば計算します。テンプレートでは匿名の訪問者へのログインリンクや、ログインしているユーザーへのログアウトリンクを表示できます。
エクストラクレジット(Extra credit)¶
- ユーザーとプリンシパルの違いは何ですか?
- データベースを使用して、
groupfinder
でプリンシパルを検索できますか? - ログインすると、ユーザー中心の情報は各リクエストに格納されるのでしょうか?この疑問に答えるために
import pdb; pdb.set_trace()
を使用します。
参考
(機械翻訳) セキュリティ, AuthTktAuthenticationPolicy, bcrypt を参照してください。