[TurboGears] ワンタイムトークンを追加するデコレータ
はじめに
ウェブアプリケーションでは、ブラウザの「戻る」ボタン対策や二重送信防止のために、ワンタイムトークンをフォームに追加することがあります。今回、TurboGearsでワンタイムトークンを扱う一連のクラスを作成しました。作成したのは、
- ワンタイムトークンを追加する@add_tokenデコレータ
- ワンタイムトークンのタグを出力するTokenFieldウィジェット
- ワンタイムトークンを検証するTokenヴァリデータ
の3つです(Tokenヴァリデータは、TokenFieldウィジェットの内部で使用されています)。
使い方
@add_tokenデコレータを、ワンタイムトークンを戻り値の辞書に追加したいコントローラのメソッドに追加します。例えば、以下のようにします。
(略) @expose() @add_token() def index(self, **kw): (略)
@add_tokenデコレータは、3つの引数をとることができます。
- name: 戻り値の辞書で、ワンタイムトークンのキーになります。
- session_name: ワンタイムトークンを保存するセッション変数のキーです。
- form_name: ブラウザでワンタイムトークンを保持する、hiddenフィールドの名前です。
フォームでは、TokenFieldウィジェットを生成し、
token = TokenField()
表示します。
${token.display(tg_token)} <p py:if="error_for(token)" py:content="error_for(token)"></p>
TokenFieldクラスは、コンストラクタに2つの引数をとることができます。
- field_name: hiddenフィールドの名前です。@add_tokenデコレータのform_name引数の値と同じ値にして下さい。
- session_name: ワンタイムトークンを保存しているセッション変数のキーです。@add_tokenデコレータのsession_nameの値と同じ値にして下さい。
ソースコード
@add_tokenデコレータ
@add_tokenデコレータは、以下のようになっています。
from turbogears import decorator import random import sys import cherrypy def add_token(name="tg_token", session_name=None, form_name=None): if session_name == None: session_name = name if form_name == None: form_name = name def entangle(func): def add_token(func, *args, **kw): returnval = func(*args, **kw) if isinstance(returnval, dict): token = "%08x" % (int(sys.maxint * random.random())) returnval[name] = token cherrypy.session[session_name] = token if hasattr(cherrypy.request, "input_values"): cherrypy.request.input_values[form_name] = token return returnval return add_token return decorator.weak_signature_decorator(entangle)
TokenFieldウィジェット
TokenFieldウィジェットのソースコードは、以下の通りです。
from turbogears import widgets class TokenField(widgets.HiddenField): def __init__(self, field_name=None, session_name=None): if field_name == None: field_name = "tg_token" if session_name == None: session_name = field_name super(TokenField, self).__init__(name=field_name, validator=Token(name=session_name))
TokenFieldウィジェットは、内部に以下のTokenヴァリデータを使っています。
from turbogears import validators import thread import cherrypy class Token(validators.FancyValidator): messages = { "invalidToken": u"フォームが二重に送信されました。操作をやり直して下さい。", } lock = thread.allocate_lock() def __init__(self, name="tg_token", *args, **kw): super(Token, self).__init__(*args, **kw) self.name = name def validate_python(self, value, state): self.__class__.lock.acquire() try: if (not self.name in cherrypy.session) \ or (cherrypy.session[self.name] != value): raise validators.Invalid( self.message("invalidToken", {}), value, state) finally: self.__class__.lock.release() del cherrypy.session[self.name]
所感
@add_tokenデコレータでは、裏技のようなことをやっています。cherrypy.request.input_valuesを使っているのがそれで、これは@validateデコレータ内でフォームから送信された内容をコピーしてとっておくための変数なのですが、ドキュメントにないこれを使っていいものかどうか、不明です。TurboGearsのバージョンが上がったら、使えなくなるかもしれません。
@add_tokenデコレータの実装については、...実はあまりよく分かっていません。ヴァリデータを付加すると関数のシグネチャが変わってしまうのがよくなくて、それを解決するのがThe decorator moduleで、これをヒントにして作ったのがturbogears.decoratorモジュールだというのは分かるのですが、weak_signature_decorator関数から先を追いきれていないです。@add_tokenデコレータは、@validateデコレータや@exposeデコレータを見ながら真似しました。