20: 認証付きログイン(20: Logins with authentication)

ユーザのリストに対してユーザ名とパスワードを認証するログイン用のビュー。

Background

ほとんどのWebアプリケーションにはWebブラウザ経由でコンテンツを追加/編集/削除できるURLがあります。アプリケーションに security を追加する時です。こ最初のステップでログイン機能を導入します。プラグイン可能なユーザストレージにPyramidの豊富な機能を使用して、ログインしてログアウトします。

次のステップでは、認証セキュリティーステートメントによるリソースの保護について紹介します。

目的(Objectives)

  • Pyramidの認証の概念を紹介します。
  • ログイン、ログアウトのビューを作成します。

手順(Steps)

  1. ビューのクラスを出発点として使用します

    $ cd ..; cp -r view_classes authentication; cd authentication
    
  2. 依存関係として authentication/setup.pybcrypt を追加します:

     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
          """,
    )
    
  3. プロジェクトを開発者モードでインストールできました:

    $ $VENV/bin/pip install -e .
    
  4. セキュリティハッシュをコードに入れるのではなく、設定ファイル authentication/development.initutorial.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
    
  5. 認証(および今のところ認証ポリシー)を取得して、ログインするには authentication/tutorial/__init__.pyconfigurator で以下のようにします:

     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()
    
  6. 「認証ポリシーコールバック」を提供することによって。ユーザー情報を見つけられる 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, [])
    
  7. 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)
    
  8. ログイン用のテンプレート 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>
    
  9. ログイン、ログアウトのボックスを提供する``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>
    
  10. Pythonアプリケーションを以下のように実行します:

    $ $VENV/bin/pserve development.ini --reload
    
  11. http://localhost:6543/ をブラウザで開きます:

  12. リンク「Log In」をクリックしてください。

  13. Submit the login form with the username editor and the password editor.

  14. リンク「Log In」が「Logout」に変更されることに注意してください。

  15. リンク「Logout」をクリックしてください。

分析(Analysis)

多くのWebフレームワークとは異なり、Pyramidには、ビルトインですが認証と認可用のオプションのセキュリティモデルがふくまれています。このセキュリティシステムは柔軟で多くのニーズをサポートするのが目的です。セキュリティのモデルでは、認証(あなたが誰か)と承認(何が許可されているか)は単なるプラグインではなく分離されています。一度に1つの手順を学ぶためにユーザーを識別してログアウトするシステムを提供しています

例では、バンドルされた AuthTktAuthenticationPolicy ポリシーを使用することを選択しました。設定で有効にして、INIファイルにチケット署名の秘密を提供しました。

ビューのクラスはログインビューを成長させました。 GET リクエストで到達したときにログインフォームを返しました。 POST 経由で到達したとき、が設定に登録した 「groupfinder」は呼び出し可能なファイルに対して、送信されたユーザー名とパスワードを処理しました。

hash_password 関数はパスワードをプレーンテキストで保存するのではなく、 bcrypt をかいしてユーザーのパスワードに 一方向のSALTハッシュアルゴリズムを使用します。これはセキュリティの「ベストプラクティス」とみなされます。

注釈

システム上で問題がある場合は、bcrypt の代替ライブラリがあります。ライブラリが安全にパスワードを格納するために承認されたアルゴリズムを使用していることを確認してください。

check_password 関数はサブミットされたパスワードとデータベースに格納されているユーザーのパスワードの2つのハッシュ値を比較します。ハッシュされた値が同等である場合は認証されます。そうでない場合は認証は失敗します。

テンプレートでは、ビュークラスから logged_in の値を取得しました。これを使用してログインしているユーザーがあれば計算します。テンプレートでは匿名の訪問者へのログインリンクや、ログインしているユーザーへのログアウトリンクを表示できます。

エクストラクレジット(Extra credit)

  1. ユーザーとプリンシパルの違いは何ですか?
  2. データベースを使用して、groupfinder でプリンシパルを検索できますか?
  3. ログインすると、ユーザー中心の情報は各リクエストに格納されるのでしょうか?この疑問に答えるために import pdb; pdb.set_trace() を使用します。

参考

(機械翻訳) セキュリティ, AuthTktAuthenticationPolicy, bcrypt を参照してください。