読者です 読者をやめる 読者になる 読者になる

Blogを作る(6)AuthKitを使った認証処理

Pylons

 誰でも投稿でき、削除できるようでは、Blogとは言えないので、最低限の認証機能を組み込みたいと思います。
 Pylonsでの認証処理には、AuthKitというライブラリを使用します。以下のチュートリアルを参考にしました。

Pylons Wiki内のAuthKitについてのドキュメント
http://pylonshq.com/project/pylonshq/wiki/PylonsWithAuthKitForward


 AuthKitはPylonsとは独立して動作するWSGIミドルウェアなので、他のプロジェクトでも使用できると思います。AuthKitで何ができるかは、本家のドキュメントを参照するのが一番よいでしょう。

AuthKitのドキュメント
http://authkit.org/docs/


 AuthKitのインストールは、CheeseShop経由が行うのが一番簡単です。

$ easy_install Authkit

AuthKitの設定

 まず、今作っているPylonsアプリケーションでAuthkitを使うために、必要な設定を行います。これは、development.iniに行います。上記のチュートリアルに従って、次の項目を追加しました。

# Authkitを有効にするならば、true
authkit.enable = true
# 登録ユーザ:パスワード
authkit.users.setup = username:password
# Authkitの認証方法, digest/form/forward
authkit.method = forward
# ログイン画面のURL
authkit.signin = /account/signin
# ログアウト画面のURL
authkit.cookie.signout = /account/signin
# 認証クッキーの名称
authkit.cookie.name = PYLONSESSIONID
# クッキーの値の改竄検出に使われる?
authkit.cookie.secret = SECRET


 認証方法を設定するauthkit.methodの項目は、Basic認証を使うdigest、AuthKit組み込みのログインフォームを使うformが最も手軽な方法でしょうが、後々のことを考えて、未ログイン時に規定のページにリダイレクトさせる、forwardを選択しています。


 secretには、次の方法でランダムな文字列を用意しました。

>>> import string as s
>>> import random
>>> ''.join([random.choice(s.letters + s.digits) for x in xrange(30)])

ミドルウェアの設定

 前述の通りAuthKitはWSGIミドルウェアなので、blogtutorial/config/middleware.pyを修正して、このミドルウェアを組み込みます。

def make_app(global_conf, full_stack=True, **app_conf):
    """Create a WSGI application and return it"""

    # ... 略

    if asbool(full_stack):
        # Change HTTPExceptions to HTTP responses
        app = httpexceptions.make_middleware(app, global_conf)

        # AuthKitの設定を追加
        import authkit.authenticate
        app = authkit.authenticate.middleware(app, config_paste=app_conf)

        # Error Handling
        app = ErrorHandler(app, global_conf, error_template=error_template, **config.errorware)

 このミドルウェアは、認証クッキーを調べて、ログイン済みならばWSGIのenvironにREMOTE_USERというキーで値を設定する、保護されたアクションがNotAuthenticatedErrorが投げたならば、それをキャッチして、ログイン画面に転送する、という仕事をしています。ミドルウェアを組み込んだだけでは、実際の認証処理は行われないので、

  1. ログイン、ログアウト画面に相当するビューとコントローラ
  2. アクション、リソースの保護

を自分でコーディングする必要があります。

認証アクションを作成

 まず認証を行うコントローラを作成します。これは普通のcontrollerで、restcontrollerではありません。

$ paster controller account

 このコマンドで作ったAccountControllerの、signin, signoutメソッドを実装します。

# -*- coding: utf-8 -*-
from blogtutorial.lib.base import *

from authkit.users import UsersFromString

from formencode import api
from formencode import validators
from formencode.schema import Schema

class SigninSchema(Schema):
    allow_extra_fields = True
    username = validators.String(not_empty=True, strip=True)
    password = validators.String(not_empty=True, strip=True)

class AccountController(BaseController):
    def __before__(self):
        model.session_context.current.clear()

    def signin(self):
        if request.method == 'POST':
            try:
                schema = SigninSchema()
                form_result = schema.to_python(dict(request.params))
                username = form_result['username']
                password = form_result['password']
                users = UsersFromString(request.environ['paste.config']['app_conf']['authkit.users.setup'])

                if username in users.passwords and password == users.passwords[username]:
                    request.environ['paste.auth_tkt.set_user'](username)
                    return redirect_to('/entries')
                else:
                    c.message = _('Bad username or password')

            except api.Invalid, errors:
                c.message = _('You must specify a username and password')

        return render_response('account/signin.myt')

    def signout(self):
        request.environ['paste.auth_tkt.logout_user']()
        redirect_to('/entries')

 ポイントは、signinメソッド内のrequest.environ['paste.auth_tkt.set_user']と、signoutメソッド内のrequest.environ['paste.auth_tkt.logout_user']です。
 前者は、呼び出し可能型で、任意のオブジェクトを与えて呼び出し、AuthKitにログインが完了したことをコールバックします。引数には、ユーザ定義のUserオブジェクト、あるいはauthkit.standard.modelsで定義されているUserオブジェクトを与えるのが一般的な使い方だと推測されますが、ここでは、たんにユーザIDを示す文字列を与えています。
 後者のrequest.environ['paste.auth_tkt.logout_user']も呼び出し可能型で、こちらは引数なしで呼び出し、ログアウトが完了したことをコールバックします。


 次に、ログイン画面のテンプレートを、blogtutorial/templates/account/signin.mytとして追加します。

<h2><% _('Sign in') %></h2>
% if c.message:
<p><% c.message %></p>
% #endif
<% h.start_form(h.url(action='signin')) %>
<% h.hidden_field('forward', value=c.forward) %>
<p><label><% _('User name') %></label>
<% h.text_field('username') %>
</p>
<p><label><% _('Password') %></label>
<% h.password_field('password') %>
</p>
<p><% h.submit(_('Sign in')) %></p>
<% h.end_form() %>


 最後に、development.iniで設定したログイン、ログアウトURLに、アクションを対応付けるために、blogtutorial/config/routing.pyを編集します。

    # routing.pyのmake_mapに追加
    # authentication
    map.connect('account/:action', controller='account')

 http://localhost:5000/account/signinにアクセスして、ログイン画面が表示されれば、ここまでの手順は成功です。

リソース、アクションを保護する

 リソースを保護するには、該当するアクションで、未ログイン時にNotAuthenticatedError例外を投げる必要があります。これには、コントローラの基底クラスで認証処理を行う方法と、デコレータを使う方法が考えられますが、ここでは、後者を選択しました。
 今のところ認証を必要とするのは、EntriesControllerだけですので、次のデコレータをblogtutorial/controllers/entries.pyに追加しました。

from decorator import decorator

def login_required(func, *args, **kwds):
    if 'REMOTE_USER' not in request.environ:
        raise NotAuthenticatedError('Not authenticated')
    return func(*args, **kwds)
login_required = decorator(login_required)

 decoratorライブラリを使っています。


 このデコレータを、認証を必要とするアクション、具体的には、new, create, edit, update, confirm_delete, deleteメソッドに適用します。

class EntriesController(BaseController):
    # ... 略

    @login_required
    def new(self, format='html'):
        """GET /new: Form to create a new item."""
        # url_for('new_entry')
        return render_response('entries/new.myt')

 これで、未ログイン時に、http://localhost:5000/entries/newにアクセスした場合には、自動的にログインページ(http://localhost:5000/account/login)にリダイレクトされるようになりました。

ログイン状態にあわせて表示を変える

 ログインしている時だけ、新規作成画面へのリンクを表示するようにテンプレートを変更します。未ログイン時には、request.environ['REMOTE_USER']がセットされないので、この値を調べています。

% if request.environ.has_key('REMOTE_USER'):
<% h.link_to(_('Post a new entry'), url=h.url_for('new_entry')) %>
% #endif

 同様に、編集画面へのリンクも制限します。

% if request.environ.has_key('REMOTE_USER'):
<% h.link_to(_('Edit'), h.url_for('edit_entry', id=c.entry.id)) %>
% #endif

 また、templates/autohandlerを変更して、ここにログイン画面へのリンクを追加しました。

<div id="header">
<h1>My blog</h1>
% if request.environ['pylons.routes_dict']['controller'] != 'account':
<p>
% if request.environ.has_key('REMOTE_USER'):
<% _('Hello, %s') % request.environ['REMOTE_USER'] | h %>
| <% h.link_to(_('Sign out'), h.url(controller='account', action='signout', id=None)) %>
% else:
<% h.link_to(_('Sign in'), h.url(controller='account', action='signin')) %>
% #endif
</p>
% # endif
</div><!-- end header -->

 request.environ['pylons.routes_dict']['controller']を調べているのは、ログイン画面では、"Hello, username!"というログイン済みのメッセージや、"Sign in"というリンクを表示させないようにするためです。Myghtyテンプレートの使い方がもっと分かれば、もう少し美しい方法もありそうですが・・・