[TurboGears] ワンタイムトークンを追加するデコレータ

はじめに

ウェブアプリケーションでは、ブラウザの「戻る」ボタン対策や二重送信防止のために、ワンタイムトークンをフォームに追加することがあります。今回、TurboGearsでワンタイムトークンを扱う一連のクラスを作成しました。作成したのは、

  1. ワンタイムトークンを追加する@add_tokenデコレータ
  2. ワンタイムトークンのタグを出力するTokenFieldウィジェット
  3. ワンタイムトークンを検証するTokenヴァリデータ

の3つです(Tokenヴァリデータは、TokenFieldウィジェットの内部で使用されています)。

使い方

@add_tokenデコレータを、ワンタイムトークンを戻り値の辞書に追加したいコントローラのメソッドに追加します。例えば、以下のようにします。

(略)
    @expose()
    @add_token()
    def index(self, **kw):
(略)

@add_tokenデコレータは、3つの引数をとることができます。

  1. name: 戻り値の辞書で、ワンタイムトークンのキーになります。
  2. session_name: ワンタイムトークンを保存するセッション変数のキーです。
  3. form_name: ブラウザでワンタイムトークンを保持する、hiddenフィールドの名前です。

フォームでは、TokenFieldウィジェットを生成し、

token = TokenField()

表示します。

${token.display(tg_token)}
<p py:if="error_for(token)" py:content="error_for(token)"></p>

TokenFieldクラスは、コンストラクタに2つの引数をとることができます。

  1. field_name: hiddenフィールドの名前です。@add_tokenデコレータのform_name引数の値と同じ値にして下さい。
  2. 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デコレータを見ながら真似しました。