編集画面でのTicketの導入について再考。
というか初めて考えているわけですが。
YakiBikiの記事の編集画面は、Xhwlayが当初予定しているよりも単純で、それ故にXhwlayが適用できない形になった。
えっと、Xhwlayが「向いている」のはこんな画面フローです。
(1) 詳細 --> (eventA) --> (2) 更新フォーム --> (eventB) --> (3) 確認フォーム --> (eventC:更新) --> (1) へ戻る --> (eventD) --> (4) 削除確認フォーム --> (eventE:削除) --> (1) 或いは一覧画面へ戻る
IDを総当たりされてeventCまたはeventEを送り込まれても、未発行のIDであれば新規フローとして(1)に初期化され、そのためeventC/eventEは(1)が受け付けないイベントとして無視されます。
次のようなフローにはXhwlayは弱いです。
(1) 更新フォーム --> (eventA:更新) --> (2) 更新完了お知らせ
2画面で、最初の画面で発生するイベントが破壊的な操作を行う場合です。これですと、IDを総当たりされてeventAが送り込まれると、新規フローに初期化されます。ところが、(1)はeventAを受け付けられますのでeventAが発動してしまい、破壊的操作が実行され、そのまま(2)に移ってしまいます。
ですので、画面フローをどう繋げるかが、Xhwlay(多分Pieceも例外じゃないとは思うのですが)を活かせるか否かの分かれ目になります。
逆に上のような2画面、あるいは次のような1画面構成の場合、状態保持云々よりは単純にいわゆるCSRF対策向けのTicket機構を導入した方が良いように思われます。
(1) 編集フォーム --- (POST:更新) --- (1) へ戻る
実はYBの記事の編集画面などが全部このパターンです。つまり、Xhwlayが適用できない。
えっと、つまり、ID総当たりされた場合Xhwlayではフローを「作って」しまうのです。望ましいのは「未発行のIDは拒否」です。うーん、Xhwlay自体も、未発行のIDが来たらIDを再生成して返す位の事はさせたいですね。
それはそれとして、また、二重POSTを判別する為には、使い終わったTicketも暫くは補完しておかなければなりません。おそらく、直近N個のTicketは覚えておき、新しいのが来たら古いのを捨てていくようにする必要があるでしょう。Xhwlayの場合、BookmarkがEndに達したフロー情報は即座に破棄していますので、これができません。あと、有効期限も必要です。Xhwlayも必要かな・・・。
イメージとしてはこんな感じです。"new"というスロットと"used"というスロットがあり、usedにはN個、Ticketを格納できるとします。今回は3個とします。
今、新しいTicketを一つ請求します。
<new> [ Ticket#1 ] <used> [ ][ ][ ]
もう一つTicketを新規請求します。
<new> [ Ticket#1 ][ Ticket#2 ] <used> [ ][ ][ ]
ここでTicket#1を使用済みにすると・・・
<new> [ Ticket#2 ] <used> [ Ticket#1 ][ ][ ]
となります。この時点で再度Ticket#1を使ったリクエストがあっても、二重送信として無視すればいいわけです。
暫く後このような状態になったとします。
<new> [ Ticket#4 ][ Ticket#5 ] <used> [ Ticket#3 ][ Ticket#2 ][ Ticket#1 ]
ここでTicket#4が使用済みになると、usedスロットから一番古いTicket#1が破棄され、代わりにTicket#4がusedスロットに入ります。
<new> [ Ticket#5 ] <used> [ Ticket#4 ][ Ticket#3 ][ Ticket#2 ]
・・・とまぁ、こんな感じの動きをしてくれれば最高ですね。ちなみにこの状態でTicket#1のリクエストが来た場合、二重POSTではなく、単に未発行IDとして無視します。
あと、「Ticketは未使用か?」と「Ticketを使用済みにする」はトランザクションでロックしなければなりません。
というのも、もしロックせず、バラバラで動かせるようにしてしまうと次のように、僅差で同じIDのリクエストが通ってしまう可能性があるからです。
<t1> reqA(ticket#1) ---> 「Ticketは未使用か?」:Yes <t2> reqA(ticket#1) ---> 「Ticketは未使用か?」:Yes ---> 「Ticketを使用済みにする」:処理開始 reqB(ticket#1) ---> 「Ticketは未使用か?」:Yes(タイミングの問題) <t3> reqA(ticket#1) ---> 「Ticketは未使用か?」:Yes ---> 「Ticketを使用済みにする」:Done ---> ... reqB(ticket#1) ---> 「Ticketは未使用か?」:Yes ---> 「Ticketを使用済みにする」:処理開始
きちんとロックし、使用済みに更新する処理が完了するまでは他のrequestはwaitさせるようにする必要があります。
<t1> reqA(ticket#1) ---> (LOCK_EX){「Ticketは未使用か?」:Yes } <t2> reqA(ticket#1) ---> (LOCK_EX){「Ticketは未使用か?」:Yes ---> 「Ticketを使用済みにする」:処理開始 } reqB(ticket#1) ---> (WAIT) <t3> reqA(ticket#1) ---> (LOCK_UN){「Ticketは未使用か?」:Yes ---> 「Ticketを使用済みにする」:Done } ---> ... reqB(ticket#1) ---> (LOCK_EX){「Ticketは未使用か?」:No }
・・・というようなのを実装しないとだなーと思いました。が、ちょっとまだ見えない部分もありますので、当面はTicket無しで進めたいと思います。一通り出揃ってから改めて考えれば良いかな、と。それまではCSRFもろ喰らう事になりますが。