HOME | Python |
bottle.py には セッション管理機能はないのですが、beaker と組み合わせてセッション管理をすることができます。 この記事ではセッションを用いた実用的なアクセスコントロールについて述べます。
以下、簡単に記載します。
pip install bottle pip install beakerでインストールできます。
sqlite での create table 文は以下のようになります。
create table users( uid text primary key not null, pw text not null, role text not null )
関数名 | 機能 |
---|---|
form_get(k) | POST メソッドで渡されたフォームから k の値を取り出す |
session_get(k, delete=False) | セッションから k の値を取り出す。delete == True のときは取りだしたあと削除する。 |
session_set(k, v) | セッションの項目 k に v を保持する |
get_role(uid, pw) | DB に問い合わせて、uid, pw をもつユーザの role を返す。存在しない場合には None を返す。 |
001: @btl.get('/login') 002: @btl.view('login') 003: def login_form(): 004: return {'message':session_get('message', True)} 005: 006: 007: @btl.post('/login') 008: @btl.view('login') 009: def login(): 010: uid=form_get('uid') 011: pw=form_get('pw') 012: role=get_role(uid, pw) 013: if role is not None: 014: session_set('uid', uid) 015: session_set('role', role) 016: session_set('message', 'こんにちは、{} さん'.format(uid)) 017: return btl.redirect('/') 018: else: 019: return {'message':'ID か Password が間違っています。'} 020: 021: 022: 023: @btl.route('/logout') 024: def logout(): 025: s = btl.request.environ['beaker.session'] 026: s.delete() 027: return btl.redirect('/login')
001: @btl.route('/') 002: @btl.view('index') 003: def index(): 004: '''TOP page''' 005: role=session_get('role') 006: if role is not None: 007: return {'message':session_get('message', True), 008: 'role':role} 009: else: 010: session_set('message', 'ログインが必要です') 011: return btl.redirect('/login')
001: @btl.route('/user') 002: @btl.view('users') 003: def users(): 004: '''List of users''' 005: role=session_get('role') 006: if role=='admin': 007: return {'users':users()} 008: else: 009: session_set('message', 'admin でログインが必要です') 010: return btl.redirect('/login')
ただ、上のコードを見るとわかるように、アクセス制御のために同じような処理が index() と users() に書かれています。 実際のサイトでは、この処理を多くの関数に書く必要があるので、いちいちコーディングしていくのは非効率です。
そこで、次の節でデコレータを使ってコードを短くすることを考えます。
アクセス制御のためのデコレータを作るクラスを Auth とします (実装は 3.4 で示します)。 まず、Auth.config() でデータベースにアクセスして role を取得する関数を設定し、 それから、デコレータ (ここでは req_admin, req_login) を作成します。 Auth.config() では次の7つの関数を設定できます。そのうち、DB にアクセスして role を取得するメソッド (get_role_from_db) は必ず設定する必要があります。
デコレータを定義するときは、role, 権限がない時に表示されるエラーメッセージ, 権限がない時のリダイレクト先を指定します。
001: Auth.config(get_role_from_db=ut.get_role) 002: 003: # decorators for access controll 004: req_admin=Auth(role='admin', message='Only admin users can access this page.') 005: req_login=Auth()
Auth.config() のキーワードパラメター
メソッド名 | 機能 |
---|---|
get_role_from_db | uid, pw を引数にとり、DB に保持されている role を返す。必須 |
logout | セッション情報をクリアする。省略可 |
get_role | セッションから role の情報を取得する。省略可 |
get_uid | セッションから uid の情報を取得する。省略可 |
set_role | セッションに role を設定する。省略可 |
set_uid | セッションに uid を設定する。省略可 |
set_message | セッションにメッセージを設定する。省略可 |
デコレータ作成時のキーワードパラメター
パラメータ名 | デフォルト | 説明 |
---|---|---|
role | None | 権限 |
message | 'login required.' | 権限がない時にセッション変数 'message' に設定される文字列 |
failure_redirect | '/login' | 権限がない時のリダイレクト先 |
アクセス制御をデコレータで外出しにすると、TOP 画面とユーザ一覧画面の関数は以下のようになります。 関数本体は1行でかけてしまいます。 デコレータの順番は、上からルーティング、アクセス制御、レンダリングです。この順番で書かないと動きません。
001: @btl.route('/') 002: @req_login 003: @btl.view('index') 004: def index(): 005: return {'message':ut.session_get('message', True), 006: 'role':Auth.get_role()} 007: 008: 009: @btl.route('/user') 010: @req_admin 011: @btl.view('users') 012: def users(): 013: return {'users':ut.users()}また、ログイン、ログアウト画面は以下のようになります。
001: @btl.post('/login') 002: @btl.view('login') 003: def login(): 004: uid,pw = [ut.form_get(x) for x in ('uid', 'pw')] 005: if Auth.login(uid,pw): 006: ut.session_set('message', 'こんにちは、{} さん'.format(uid)) 007: return btl.redirect('/') 008: else: 009: return {'message':'ID か Password が間違っています。'} 010: 011: 012: @btl.route('/logout') 013: def logout(): 014: Auth.logout() 015: return btl.redirect('/login')
このモジュールは、uid と pw を引数に与えて DB から role を取得できる関数があれば、バックエンドの DB の種類にかかわらず使うことができます。 ここでは sqlite を使いましたが、他の RDBM でもよいし、Key-Value 型のストレージも使えます。
39--48 行目の __call__() がデコレータとして呼ばれたときの挙動で、権限があるときに引数で与えられた関数を呼び出し、 そうでなければ、/login に遷移する関数を返します。
001: import bottle as btl 002: import session_utils as ut 003: from functools import partial, wraps 004: 005: class Auth: 006: '''generating decorators for access control''' 007: 008: # class attribute 009: CLS_ATTR=[ 010: ('get_role_from_db', lambda uid,pw:None), # lambda uid,pw: role if a record having (uid, pw) exists in the db else None 011: ('logout', ut.logout), # method to clear the session 012: ('set_uid', partial(ut.session_set, 'uid')), # method to set uid into the session 013: ('set_role', partial(ut.session_set, 'role')), # method to set role into the session 014: ('get_uid', lambda:ut.session_get('uid')), # method to get uid from the session 015: ('get_role', lambda:ut.session_get('role')), # method to get role from the session 016: ('set_message', partial(ut.session_set,'message')), 017: ] 018: 019: @classmethod 020: def config(cls, **kw): 021: '''setting static methods''' 022: for k,v in cls.CLS_ATTR: 023: setattr(cls, k, staticmethod(kw.get(k,v))) 024: 025: @classmethod 026: def login(cls, uid, pw): 027: role=cls.get_role_from_db(uid, pw) 028: if role: 029: cls.set_uid(uid) 030: cls.set_role(role) 031: return role 032: 033: def __init__(self,**kw): 034: '''kw parameters are role, message and failure_redirect''' 035: for k,v in [('message', 'Login required'), ('failure_redirect', '/login')]: 036: setattr(self, k, kw.get(k, v)) 037: self.is_auth=(lambda :kw['role']==self.get_role()) if 'role' in kw else self.get_role 038: 039: def __call__(self, fun): 040: '''acting as a decorator''' 041: @wraps(fun) 042: def _f(*a, **k): 043: if self.is_auth(): 044: return fun(*a, **k) 045: else: 046: self.set_message(self.message) 047: return btl.redirect(self.failure_redirect) 048: return _f
uid | pw | role |
---|---|---|
john | smith | guest |
peter | norvig | admin |
実は bottle.py にはアクセス制御用の plug-in があるのですが、 自分で書いても大した手間でないのと、自作のものの方が、自分で使う分には便利なので、書いてみました。
bottle.py は徐々に発展しており、便利な plug-in も増えてきているので、 小規模なサイトであれば業務用にも使えるようになってきています。
HOME | Python |